synchronisation du contenu avec la DB et amélioration du CMS

- Migration du blog et des interviews des données mockées vers PostgreSQL (Prisma).
- Refonte des pages publiques en Server Components pour de meilleures performances/SEO.
- Intégration d'un éditeur de texte riche (React Quill) avec titres H1-H6 et séparateurs.
- Correction des erreurs de validation Prisma liées aux paramètres asynchrones (Next.js 15+).
- Correction des problèmes d'affichage des modales de suspension via React Portals.
- Installation de @tailwindcss/typography et correction du débordement de texte (break-words).
- Implémentation des actions de modération (suspension/révocation) pour les utilisateurs et boutiques.
This commit is contained in:
2026-04-12 18:59:20 +02:00
parent b73939d381
commit 887030ee47
84 changed files with 5577 additions and 15128 deletions

View File

@@ -1,22 +1,24 @@
"use client";
import React, { useEffect } from 'react';
import React from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { notFound } from 'next/navigation';
import { ArrowLeft, Share2, Play, Calendar, User, Building2 } from 'lucide-react';
import { MOCK_INTERVIEWS } from '../../../lib/mockData';
import { InterviewType } from '../../../types';
import { prisma } from '../../../lib/prisma';
import { InterviewType } from '@prisma/client';
const InterviewDetailPage = () => {
const { id } = useParams();
const interview = MOCK_INTERVIEWS.find(i => i.id === id);
interface Props {
params: Promise<{ id: string }>;
}
useEffect(() => {
window.scrollTo(0, 0);
}, [id]);
export default async function InterviewDetailPage({ params }: Props) {
const { id } = await params;
const interview = await prisma.interview.findUnique({
where: { id }
});
if (!interview) return null;
if (!interview) {
notFound();
}
// Helper to get YouTube embed URL
const getEmbedUrl = (url: string) => {
@@ -56,7 +58,7 @@ const InterviewDetailPage = () => {
</div>
<div className="flex items-center">
<Calendar className="w-4 h-4 mr-2 text-brand-500" />
{interview.date}
{new Date(interview.date).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
</div>
</div>
</div>
@@ -73,7 +75,7 @@ const InterviewDetailPage = () => {
className="w-full h-full"
></iframe>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-900">
<div className="w-full h-full flex items-center justify-center bg-gray-900 min-h-[300px]">
<p className="text-white">Vidéo non disponible</p>
</div>
)}
@@ -86,11 +88,9 @@ const InterviewDetailPage = () => {
)}
{/* Description / Article Content */}
<div className="prose prose-lg prose-orange max-w-none mx-auto text-gray-600">
<div className="prose prose-lg prose-orange max-w-none mx-auto text-gray-600 break-words overflow-hidden">
{!isVideo && interview.content ? (
interview.content.split('\n').map((paragraph, idx) => (
<p key={idx}>{paragraph}</p>
))
<div dangerouslySetInnerHTML={{ __html: interview.content }} />
) : (
<p className="text-xl font-serif italic text-gray-500 border-l-4 border-brand-500 pl-6 py-2">
{interview.excerpt}
@@ -108,6 +108,5 @@ const InterviewDetailPage = () => {
</div>
</div>
);
};
}
export default InterviewDetailPage;