1. Système d'Analytics (Nouveauté Majeure)
Modèle de Données : Ajout de la table AnalyticsEvent dans prisma/schema.prisma pour suivre les vues de pages, les clics et le temps de rétention (DWELL_TIME). API Backend : Création de la route app/api/analytics/route.ts pour enregistrer les événements en base de données. Tracking Client : Création du composant AnalyticsTracker.tsx (intégré dans le layout principal) qui capture automatiquement : Le changement de page (Page Views). Les clics sur les boutons et liens importants. Le temps passé sur chaque page. 2. Intégration Base de Données (PostgreSQL/Prisma) Fiches Entreprises : Mise à jour de app/directory/[id]/page.tsx pour récupérer les données réelles depuis PostgreSQL via Prisma au lieu d'utiliser les données de test (mockData). Navigation Dashboard : Correction du bouton "Voir ma fiche" dans le tableau de bord pour qu'il redirige correctement vers le profil public de l'entrepreneur. 3. Administration & Metrics Nouvelle Application Admin : Initialisation d'un dossier admin/ (application Next.js séparée) destinée à la gestion de la plateforme et à la visualisation des statistiques (Metrics & Analytics). 4. Authentification & Inscription Nouvelle Page : Création de app/register/page.tsx pour permettre l'inscription des nouveaux utilisateurs. Gestion de Session : Amélioration de UserProvider.tsx pour une meilleure gestion de l'état utilisateur à travers l'application. 5. Maintenance & Types Mise à jour des types globaux dans types.ts et rafraîchissement du package-lock.json suite à l'ajout de dépendances.
This commit is contained in:
187
admin/src/components/InterviewForm.tsx
Normal file
187
admin/src/components/InterviewForm.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { createInterview, updateInterview } from '@/app/actions/interview';
|
||||
import { Loader2, ArrowLeft, Save, Video, FileText } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { InterviewType } from '@prisma/client';
|
||||
|
||||
interface Props {
|
||||
initialData?: any;
|
||||
}
|
||||
|
||||
export default function InterviewForm({ initialData }: Props) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const data: any = {
|
||||
title: formData.get('title') as string,
|
||||
guestName: formData.get('guestName') as string,
|
||||
companyName: formData.get('companyName') as string,
|
||||
role: formData.get('role') as string,
|
||||
type: formData.get('type') as InterviewType,
|
||||
thumbnailUrl: formData.get('thumbnailUrl') as string,
|
||||
videoUrl: formData.get('videoUrl') as string || null,
|
||||
excerpt: formData.get('excerpt') as string,
|
||||
content: formData.get('content') as string || null,
|
||||
duration: formData.get('duration') as string || null,
|
||||
};
|
||||
|
||||
startTransition(async () => {
|
||||
const result = initialData
|
||||
? await updateInterview(initialData.id, data)
|
||||
: await createInterview(data);
|
||||
|
||||
if (result.success) {
|
||||
router.push('/blog');
|
||||
router.refresh();
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/blog" className="p-2 bg-slate-800 rounded-lg text-slate-400 hover:text-white transition-colors">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-white">
|
||||
{initialData ? 'Modifier l\'interview' : 'Nouvelle Interview'}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="card space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Titre de l'interview</label>
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={initialData?.title}
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="Ex: Entreprise X : Une success story..."
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Nom de l'invité</label>
|
||||
<input
|
||||
name="guestName"
|
||||
defaultValue={initialData?.guestName}
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="Ex: Sarah Koné"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Entreprise</label>
|
||||
<input
|
||||
name="companyName"
|
||||
defaultValue={initialData?.companyName}
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="Ex: TechAfrica"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Rôle / Poste</label>
|
||||
<input
|
||||
name="role"
|
||||
defaultValue={initialData?.role}
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="Ex: Fondatrice & CEO"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Type d'interview</label>
|
||||
<select
|
||||
name="type"
|
||||
defaultValue={initialData?.type || 'VIDEO'}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
>
|
||||
<option value="VIDEO">Vidéo</option>
|
||||
<option value="ARTICLE">Article (Texte)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Durée (Optionnel)</label>
|
||||
<input
|
||||
name="duration"
|
||||
defaultValue={initialData?.duration}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="Ex: 12 min"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">URL de la miniature (Thumbnail)</label>
|
||||
<input
|
||||
name="thumbnailUrl"
|
||||
defaultValue={initialData?.thumbnailUrl}
|
||||
required
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">URL Vidéo (Si type Vidéo)</label>
|
||||
<input
|
||||
name="videoUrl"
|
||||
defaultValue={initialData?.videoUrl}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
placeholder="https://youtube.com/..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Extrait (Court résumé)</label>
|
||||
<textarea
|
||||
name="excerpt"
|
||||
defaultValue={initialData?.excerpt}
|
||||
required
|
||||
rows={2}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-400">Contenu / Transcription (Optionnel)</label>
|
||||
<textarea
|
||||
name="content"
|
||||
defaultValue={initialData?.content}
|
||||
rows={8}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full btn-verify flex items-center justify-center gap-2 py-4 text-lg bg-emerald-600"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-6 h-6" />
|
||||
)}
|
||||
{initialData ? 'Enregistrer les modifications' : 'Créer l\'interview'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user