From 2e7cb65a29b23962c11eff48c25cf369cbcb20dc Mon Sep 17 00:00:00 2001 From: streaper2 Date: Wed, 22 Apr 2026 22:59:12 +0200 Subject: [PATCH] feat: initialize Prisma schema and scaffold core application modules including auth, blogs, events, and business directories --- admin/prisma/schema.prisma | 70 ++++--- admin/src/app/actions/blog.ts | 21 +- admin/src/app/actions/event.ts | 82 ++++++++ admin/src/app/actions/interview.ts | 25 ++- admin/src/app/blog/page.tsx | 119 ++++++++--- admin/src/app/event/edit/[id]/page.tsx | 20 ++ admin/src/app/event/new/page.tsx | 5 + admin/src/app/metrics/page.tsx | 89 +++++++- admin/src/components/BlogForm.tsx | 140 +++++++++---- admin/src/components/DeleteEventButton.tsx | 41 ++++ admin/src/components/EventForm.tsx | 208 +++++++++++++++++++ admin/src/components/InterviewForm.tsx | 228 +++++++++++++-------- admin/src/components/RichTextEditor.tsx | 2 +- admin/src/lib/utils.ts | 13 ++ app/afrolife/[id]/page.tsx | 145 ++++++++++--- app/afrolife/page.tsx | 109 +++++++--- app/annuaire/page.tsx | 9 +- app/api/settings/route.ts | 11 + app/blog/[id]/page.tsx | 52 ++++- app/blog/page.tsx | 2 +- app/cgu/page.tsx | 14 +- app/cgv/page.tsx | 14 +- app/dashboard/page.tsx | 15 +- app/layout.tsx | 13 +- app/login/page.tsx | 7 +- app/register/page.tsx | 6 +- components/EventTimeline.tsx | 118 +++++++++++ components/Navbar.tsx | 14 +- components/UserProvider.tsx | 10 +- prisma/schema.prisma | 70 ++++--- scratch/check_featured.js | 15 ++ scratch/check_featured.mjs | 15 ++ 32 files changed, 1392 insertions(+), 310 deletions(-) create mode 100644 admin/src/app/actions/event.ts create mode 100644 admin/src/app/event/edit/[id]/page.tsx create mode 100644 admin/src/app/event/new/page.tsx create mode 100644 admin/src/components/DeleteEventButton.tsx create mode 100644 admin/src/components/EventForm.tsx create mode 100644 admin/src/lib/utils.ts create mode 100644 app/api/settings/route.ts create mode 100644 components/EventTimeline.tsx create mode 100644 scratch/check_featured.js create mode 100644 scratch/check_featured.mjs diff --git a/admin/prisma/schema.prisma b/admin/prisma/schema.prisma index 2838107..37d2feb 100644 --- a/admin/prisma/schema.prisma +++ b/admin/prisma/schema.prisma @@ -121,32 +121,40 @@ model Offer { } model BlogPost { - id String @id @default(uuid()) - title String - excerpt String - content String - author String - date DateTime @default(now()) - imageUrl String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + title String + slug String? @unique + excerpt String + content String + author String + date DateTime @default(now()) + imageUrl String + tags String[] @default([]) + metaTitle String? + metaDescription String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Interview { - id String @id @default(uuid()) - title String - guestName String - companyName String - role String - type InterviewType - thumbnailUrl String - videoUrl String? - content String? - excerpt String - date DateTime @default(now()) - duration String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + title String + slug String? @unique + guestName String + companyName String + role String + type InterviewType + thumbnailUrl String + videoUrl String? + content String? + excerpt String + date DateTime @default(now()) + duration String? + tags String[] @default([]) + metaTitle String? + metaDescription String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Comment { @@ -288,3 +296,19 @@ model LegalDocument { content String @db.Text updatedAt DateTime @updatedAt } + +model Event { + id String @id @default(uuid()) + title String + slug String? @unique + description String @db.Text + date DateTime + location String + thumbnailUrl String + link String? + tags String[] @default([]) + metaTitle String? + metaDescription String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/admin/src/app/actions/blog.ts b/admin/src/app/actions/blog.ts index 234e817..94c2023 100644 --- a/admin/src/app/actions/blog.ts +++ b/admin/src/app/actions/blog.ts @@ -2,18 +2,27 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { generateSlug } from "@/lib/utils"; export async function createBlogPost(data: { title: string; + slug?: string; excerpt: string; content: string; author: string; imageUrl: string; + tags?: string[]; + metaTitle?: string; + metaDescription?: string; }) { try { + const slug = data.slug || generateSlug(data.title); + const post = await prisma.blogPost.create({ data: { ...data, + slug, + tags: data.tags || [], }, }); revalidatePath("/blog"); @@ -26,15 +35,25 @@ export async function createBlogPost(data: { export async function updateBlogPost(id: string, data: { title: string; + slug?: string; excerpt: string; content: string; author: string; imageUrl: string; + tags?: string[]; + metaTitle?: string; + metaDescription?: string; }) { try { + const slug = data.slug || generateSlug(data.title); + const post = await prisma.blogPost.update({ where: { id }, - data, + data: { + ...data, + slug, + tags: data.tags || [], + }, }); revalidatePath("/blog"); return { success: true, data: post }; diff --git a/admin/src/app/actions/event.ts b/admin/src/app/actions/event.ts new file mode 100644 index 0000000..07b5b47 --- /dev/null +++ b/admin/src/app/actions/event.ts @@ -0,0 +1,82 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; +import { generateSlug } from "@/lib/utils"; + +export async function createEvent(data: { + title: string; + slug?: string; + description: string; + date: Date; + location: string; + thumbnailUrl: string; + link?: string; + tags?: string[]; + metaTitle?: string; + metaDescription?: string; +}) { + try { + const slug = data.slug || generateSlug(data.title); + + const event = await prisma.event.create({ + data: { + ...data, + slug, + tags: data.tags || [], + }, + }); + revalidatePath("/blog"); + revalidatePath("/afrolife"); + return { success: true, data: event }; + } catch (error) { + console.error("Failed to create event:", error); + return { success: false, error: "Erreur lors de la création de l'événement" }; + } +} + +export async function updateEvent(id: string, data: { + title: string; + slug?: string; + description: string; + date: Date; + location: string; + thumbnailUrl: string; + link?: string; + tags?: string[]; + metaTitle?: string; + metaDescription?: string; +}) { + try { + const slug = data.slug || generateSlug(data.title); + + const event = await prisma.event.update({ + where: { id }, + data: { + ...data, + slug, + tags: data.tags || [], + }, + }); + revalidatePath("/blog"); + revalidatePath("/afrolife"); + return { success: true, data: event }; + } catch (error) { + console.error("Failed to update event:", error); + return { success: false, error: "Erreur lors de la mise à jour" }; + } +} + +export async function deleteEvent(id: string) { + try { + await prisma.event.delete({ + where: { id }, + }); + revalidatePath("/blog"); + revalidatePath("/afrolife"); + return { success: true }; + } catch (error) { + console.error("Failed to delete event:", error); + return { success: false, error: "Erreur lors de la suppression" }; + } +} diff --git a/admin/src/app/actions/interview.ts b/admin/src/app/actions/interview.ts index 4392120..e9fba53 100644 --- a/admin/src/app/actions/interview.ts +++ b/admin/src/app/actions/interview.ts @@ -3,9 +3,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { InterviewType } from "@prisma/client"; +import { generateSlug } from "@/lib/utils"; export async function createInterview(data: { title: string; + slug?: string; guestName: string; companyName: string; role: string; @@ -15,10 +17,19 @@ export async function createInterview(data: { content?: string; excerpt: string; duration?: string; + tags?: string[]; + metaTitle?: string; + metaDescription?: string; }) { try { + const slug = data.slug || generateSlug(data.title); + const interview = await prisma.interview.create({ - data, + data: { + ...data, + slug, + tags: data.tags || [], + }, }); revalidatePath("/blog"); return { success: true, data: interview }; @@ -30,6 +41,7 @@ export async function createInterview(data: { export async function updateInterview(id: string, data: { title: string; + slug?: string; guestName: string; companyName: string; role: string; @@ -39,11 +51,20 @@ export async function updateInterview(id: string, data: { content?: string; excerpt: string; duration?: string; + tags?: string[]; + metaTitle?: string; + metaDescription?: string; }) { try { + const slug = data.slug || generateSlug(data.title); + const interview = await prisma.interview.update({ where: { id }, - data, + data: { + ...data, + slug, + tags: data.tags || [], + }, }); revalidatePath("/blog"); return { success: true, data: interview }; diff --git a/admin/src/app/blog/page.tsx b/admin/src/app/blog/page.tsx index 13af211..40edfd1 100644 --- a/admin/src/app/blog/page.tsx +++ b/admin/src/app/blog/page.tsx @@ -1,34 +1,40 @@ import { prisma } from '@/lib/prisma'; import Link from 'next/link'; -import { Plus, BookOpen, Mic2, Trash2, Edit } from 'lucide-react'; +import { Plus, BookOpen, Mic2, Trash2, Edit, Calendar, MapPin } from 'lucide-react'; import DeleteBlogButton from '@/components/DeleteBlogButton'; import DeleteInterviewButton from '@/components/DeleteInterviewButton'; +import DeleteEventButton from '@/components/DeleteEventButton'; async function getData() { const posts = await prisma.blogPost.findMany({ orderBy: { createdAt: 'desc' } }); const interviews = await prisma.interview.findMany({ orderBy: { createdAt: 'desc' } }); - return { posts, interviews }; + const events = await prisma.event.findMany({ orderBy: { date: 'desc' } }); + return { posts, interviews, events }; } export default async function BlogCMSPage() { - const { posts, interviews } = await getData(); + const { posts, interviews, events } = await getData(); return ( -
+
-

CMS Blog & Interviews

-

Gérez le contenu éditorial de la plateforme.

+

CMS Blog, Interviews & Événements

+

Gérez le contenu éditorial et événementiel de la plateforme.

-
- +
+ Nouvel Article - + Nouvelle Interview + + + Nouvel Événement +
@@ -40,25 +46,25 @@ export default async function BlogCMSPage() {

Articles de Blog ({posts.length})

- +
- - - - + + + + - + {posts.map((post) => ( - - + - - - + +
TitreAuteurDateActionsTitreAuteurDateActions
+
{post.title}
{post.excerpt}
{post.author}{new Date(post.createdAt).toLocaleDateString('fr-FR')} + {post.author}{new Date(post.createdAt).toLocaleDateString('fr-FR')}
@@ -80,32 +86,32 @@ export default async function BlogCMSPage() {

Interviews ({interviews.length})

- +
- - - - + + + + - + {interviews.map((interview) => ( - - + - - -
InterviewéEntreprise / RôleTypeActionsInterviewéEntreprise / RôleTypeActions
+
{interview.guestName}
{interview.title}
+
{interview.companyName}
{interview.role}
- + + {interview.type} +
@@ -119,6 +125,55 @@ export default async function BlogCMSPage() {
+ + {/* Events Section */} +
+
+ +

Événements ({events.length})

+
+
+ + + + + + + + + + {events.map((event) => ( + + + + + + ))} + +
ÉvénementDate & LieuActions
+
{event.title}
+
+ {event.link ? 'Lien activé' : 'Pas de lien'} +
+
+
+ + {new Date(event.date).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })} +
+
+ + {event.location} +
+
+
+ + + + +
+
+
+
); diff --git a/admin/src/app/event/edit/[id]/page.tsx b/admin/src/app/event/edit/[id]/page.tsx new file mode 100644 index 0000000..f46f1fc --- /dev/null +++ b/admin/src/app/event/edit/[id]/page.tsx @@ -0,0 +1,20 @@ +import EventForm from "@/components/EventForm"; +import { prisma } from "@/lib/prisma"; +import { notFound } from "next/navigation"; + +interface Props { + params: Promise<{ id: string }>; +} + +export default async function EditEventPage({ params }: Props) { + const { id } = await params; + const event = await prisma.event.findUnique({ + where: { id }, + }); + + if (!event) { + notFound(); + } + + return ; +} diff --git a/admin/src/app/event/new/page.tsx b/admin/src/app/event/new/page.tsx new file mode 100644 index 0000000..15514ac --- /dev/null +++ b/admin/src/app/event/new/page.tsx @@ -0,0 +1,5 @@ +import EventForm from "@/components/EventForm"; + +export default function NewEventPage() { + return ; +} diff --git a/admin/src/app/metrics/page.tsx b/admin/src/app/metrics/page.tsx index ab4a202..d09e1d0 100644 --- a/admin/src/app/metrics/page.tsx +++ b/admin/src/app/metrics/page.tsx @@ -125,6 +125,26 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { prevMobileCount = prevDevices.find(d => d.device === 'Mobile')?._count.id || 0; } + // 5. Recent Unique IPs + const recentUniqueIPs = await prisma.analyticsEvent.groupBy({ + by: ['ip'], + where: currentWhere, + _max: { createdAt: true }, + orderBy: { _max: { createdAt: 'desc' } }, + take: 20 + }); + + const recentIPs = await Promise.all( + recentUniqueIPs.map(async (item) => { + return await prisma.analyticsEvent.findFirst({ + where: { + ip: item.ip, + createdAt: item._max.createdAt + } + }); + }) + ); + const currentMobileCount = devices.find(d => d.device === 'Mobile')?._count.id || 0; return { @@ -134,6 +154,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { browsers, totalEvents, uniqueVisitors, + recentIPs, deltas: { events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null, visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null, @@ -147,7 +168,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { export default async function MetricsPage({ searchParams: searchParamsPromise }: { searchParams: Promise<{ range?: string, from?: string, to?: string }> }) { const searchParams = await searchParamsPromise; const range = searchParams.range || 'week'; - const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, deltas } = await getMetrics(range, searchParams.from, searchParams.to); + const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas } = await getMetrics(range, searchParams.from, searchParams.to); const rangeLabels: Record = { day: 'Dernières 24h', @@ -331,7 +352,7 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }: - {/* Top Pages Table (Moved and compact) */} + {/* Top Pages Table (Restored) */}

@@ -357,6 +378,70 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:

+ + {/* Recent IP Connections Table */} +
+
+

+ Dernières Connexions (IP) +

+
+
+ + + + + + + + + + + + + {recentIPs.map((e, idx) => e && ( + + + + + + + + + ))} + +
Adresse IPLocalisationAppareil / OSNavigateurDernière PageDate / Heure
+ + {e.ip || '---.---.---.---'} + + +
+ {e.country || 'Inconnu'} + {e.city || '---'} +
+
+
+ + {e.device} + + {e.os} +
+
+ {e.browser} + + + {e.path} + + +
+ {new Date(e.createdAt).toLocaleDateString('fr-FR')} + {new Date(e.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} +
+
+
+
); diff --git a/admin/src/components/BlogForm.tsx b/admin/src/components/BlogForm.tsx index 86ab415..87d94d7 100644 --- a/admin/src/components/BlogForm.tsx +++ b/admin/src/components/BlogForm.tsx @@ -16,6 +16,10 @@ interface Props { content: string; author: string; imageUrl: string; + slug?: string | null; + tags?: string[]; + metaTitle?: string | null; + metaDescription?: string | null; }; } @@ -29,10 +33,14 @@ export default function BlogForm({ initialData }: Props) { const formData = new FormData(event.currentTarget); const data = { title: formData.get('title') as string, + slug: formData.get('slug') as string, excerpt: formData.get('excerpt') as string, content: content, author: formData.get('author') as string, imageUrl: formData.get('imageUrl') as string, + tags: (formData.get('tags') as string)?.split(',').map(t => t.trim()).filter(t => t !== ''), + metaTitle: formData.get('metaTitle') as string, + metaDescription: formData.get('metaDescription') as string, }; startTransition(async () => { @@ -51,7 +59,7 @@ export default function BlogForm({ initialData }: Props) { }; return ( -
+
@@ -63,60 +71,112 @@ export default function BlogForm({ initialData }: Props) {
-
-
+ +
+

Informations Générales

+ +
+
+ + +
+
+ + +
+
+
- +
+
- - Extrait (Excerpt) +