265 lines
11 KiB
TypeScript
265 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useTransition, useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { createEvent, updateEvent } from '@/app/actions/event';
|
|
import { Loader2, ArrowLeft, Save, Calendar, MapPin, Link as LinkIcon, Send } from 'lucide-react';
|
|
import Link from 'next/link';
|
|
import { toast } from 'react-hot-toast';
|
|
import RichTextEditor from './RichTextEditor';
|
|
import ImageUploader from './ImageUploader';
|
|
import TagInput from './TagInput';
|
|
import { ContentStatus } from '@prisma/client';
|
|
import { getAllTags } from '@/app/actions/taxonomies';
|
|
|
|
interface Props {
|
|
initialData?: any;
|
|
}
|
|
|
|
export default function EventForm({ initialData }: Props) {
|
|
const router = useRouter();
|
|
const [isPending, startTransition] = useTransition();
|
|
const [description, setDescription] = useState(initialData?.description || '');
|
|
const [thumbnailUrl, setThumbnailUrl] = useState(initialData?.thumbnailUrl || '');
|
|
const [tags, setTags] = useState<string[]>(initialData?.tags || []);
|
|
const [allExistingTags, setAllExistingTags] = useState<string[]>([]);
|
|
const [publishedAtValue, setPublishedAtValue] = useState(
|
|
initialData?.publishedAt
|
|
? new Date(initialData.publishedAt).toISOString().slice(0, 16)
|
|
: new Date().toISOString().slice(0, 16)
|
|
);
|
|
|
|
const isPublished = new Date(publishedAtValue) <= new Date();
|
|
|
|
useEffect(() => {
|
|
getAllTags().then(setAllExistingTags);
|
|
}, []);
|
|
|
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.currentTarget);
|
|
const publishedAtStr = formData.get('publishedAt') as string;
|
|
|
|
const data: any = {
|
|
title: formData.get('title') as string,
|
|
slug: formData.get('slug') as string,
|
|
location: formData.get('location') as string,
|
|
date: new Date(formData.get('date') as string),
|
|
thumbnailUrl: thumbnailUrl, // Use state value
|
|
link: formData.get('link') as string || null,
|
|
description: description || '',
|
|
tags: tags, // Use state
|
|
metaTitle: formData.get('metaTitle') as string,
|
|
metaDescription: formData.get('metaDescription') as string,
|
|
publishedAt: publishedAtStr ? new Date(publishedAtStr) : undefined,
|
|
status: formData.get('status') as ContentStatus,
|
|
};
|
|
|
|
if (!data.thumbnailUrl) {
|
|
toast.error("Une image est requise");
|
|
return;
|
|
}
|
|
|
|
startTransition(async () => {
|
|
const result = initialData
|
|
? await updateEvent(initialData.id, data)
|
|
: await createEvent(data);
|
|
|
|
if (result.success) {
|
|
toast.success(initialData ? "Événement mis à jour" : "Événement créé avec succès");
|
|
router.push('/actualites');
|
|
router.refresh();
|
|
} else {
|
|
toast.error(result.error || "Une erreur est survenue");
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto pb-20">
|
|
<div className="mb-8 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<Link href="/actualites" 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\'événement' : 'Nouvel Événement'}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-8">
|
|
<div className="card space-y-6">
|
|
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
|
<h2 className="text-xl font-semibold text-white">Informations Générales</h2>
|
|
<div className="flex items-center gap-3">
|
|
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Statut :</label>
|
|
<select
|
|
name="status"
|
|
defaultValue={initialData?.status || 'PUBLISHED'}
|
|
className="bg-slate-900 border border-slate-700 text-white text-xs rounded-full px-4 py-1.5 focus:outline-none focus:border-brand-500 appearance-none cursor-pointer hover:bg-slate-800 transition-colors"
|
|
>
|
|
<option value="DRAFT">Brouillon</option>
|
|
<option value="PUBLISHED">Publié</option>
|
|
<option value="ARCHIVED">Archivé</option>
|
|
</select>
|
|
</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">Titre de l'événement</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: Conférence AfroHub 2026"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-400">Date de l'événement (Début)</label>
|
|
<div className="relative">
|
|
<Calendar className="absolute left-3 top-3.5 w-5 h-5 text-slate-500" />
|
|
<input
|
|
name="date"
|
|
type="datetime-local"
|
|
defaultValue={initialData?.date ? new Date(initialData.date).toISOString().slice(0, 16) : ''}
|
|
required
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
</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">Lieu</label>
|
|
<div className="relative">
|
|
<MapPin className="absolute left-3 top-3.5 w-5 h-5 text-slate-500" />
|
|
<input
|
|
name="location"
|
|
defaultValue={initialData?.location}
|
|
required
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
|
|
placeholder="Ex: Abidjan, Côte-d'Ivoire / Zoom"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-400">
|
|
Date de publication {isPublished ? '(Publiée)' : '(Programmation)'}
|
|
</label>
|
|
<div className="relative">
|
|
<Send className="absolute left-3 top-3.5 w-5 h-5 text-slate-500" />
|
|
<input
|
|
name="publishedAt"
|
|
type="datetime-local"
|
|
value={publishedAtValue}
|
|
onChange={(e) => setPublishedAtValue(e.target.value)}
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<ImageUploader
|
|
label="Image (Affiche/Miniature)"
|
|
value={thumbnailUrl}
|
|
onChange={setThumbnailUrl}
|
|
name="thumbnailUrl"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-400">Lien d'inscription / info (Optionnel)</label>
|
|
<div className="relative">
|
|
<LinkIcon className="absolute left-3 top-3.5 w-5 h-5 text-slate-500" />
|
|
<input
|
|
name="link"
|
|
defaultValue={initialData?.link}
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
|
|
placeholder="https://..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-400">Description de l'événement</label>
|
|
<RichTextEditor
|
|
value={description}
|
|
onChange={setDescription}
|
|
placeholder="Détails sur l'événement, programme, intervenants..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SEO SECTION */}
|
|
<div className="card space-y-6">
|
|
<h2 className="text-xl font-semibold text-white border-b border-slate-800 pb-4">Optimisation SEO</h2>
|
|
|
|
<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">Slug (URL personnalisée)</label>
|
|
<input
|
|
name="slug"
|
|
defaultValue={initialData?.slug || ''}
|
|
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: conference-afrohub-2026"
|
|
/>
|
|
<p className="text-xs text-slate-500 italic">Laissez vide pour générer automatiquement à partir du titre.</p>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<TagInput
|
|
value={tags}
|
|
onChange={setTags}
|
|
suggestions={allExistingTags}
|
|
label="Mots-clés (tags)"
|
|
placeholder="evenement, networking, afrique..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-400">Meta Titre (SEO)</label>
|
|
<input
|
|
name="metaTitle"
|
|
defaultValue={initialData?.metaTitle || ''}
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
|
placeholder="Titre optimisé pour Google"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-400">Meta Description (SEO)</label>
|
|
<textarea
|
|
name="metaDescription"
|
|
defaultValue={initialData?.metaDescription || ''}
|
|
rows={3}
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-white focus:outline-none focus:border-indigo-500"
|
|
placeholder="Description courte pour les résultats de recherche..."
|
|
/>
|
|
</div>
|
|
</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-indigo-600"
|
|
>
|
|
{isPending ? (
|
|
<Loader2 className="w-6 h-6 animate-spin" />
|
|
) : (
|
|
<Save className="w-6 h-6" />
|
|
)}
|
|
{initialData ? 'Enregistrer les modifications' : 'Créer l\'événement'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|