- 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.
112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { ShieldAlert, X, AlertOctagon } from 'lucide-react';
|
|
import { toast } from 'react-hot-toast';
|
|
|
|
interface SuspensionModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onConfirm: (reason: string) => Promise<void>;
|
|
title: string;
|
|
subtitle: string;
|
|
}
|
|
|
|
export default function SuspensionModal({
|
|
isOpen,
|
|
onClose,
|
|
onConfirm,
|
|
title,
|
|
subtitle
|
|
}: SuspensionModalProps) {
|
|
const [reason, setReason] = useState('');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
return () => setMounted(false);
|
|
}, []);
|
|
|
|
if (!isOpen || !mounted) return null;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!reason.trim()) {
|
|
toast.error("Veuillez saisir un motif de suspension");
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
await onConfirm(reason);
|
|
setReason('');
|
|
onClose();
|
|
} catch (error) {
|
|
console.error(error);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const modalContent = (
|
|
<div className="modal-overlay" style={{ zIndex: 9999 }}>
|
|
<div className="modal-content max-w-md shadow-2xl">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-3 text-red-500">
|
|
<ShieldAlert className="w-7 h-7" />
|
|
<h2 className="text-2xl font-bold text-white">{title}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 rounded-full hover:bg-slate-800 text-slate-500 hover:text-white transition-all"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="bg-red-500/10 border border-red-500/20 p-4 rounded-xl flex gap-3 mb-6">
|
|
<AlertOctagon className="w-6 h-6 text-red-500 shrink-0" />
|
|
<p className="text-sm text-slate-300 leading-relaxed font-medium">{subtitle}</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="block text-xs font-bold text-slate-500 uppercase tracking-widest">
|
|
Motif de la suspension
|
|
</label>
|
|
<textarea
|
|
required
|
|
rows={4}
|
|
value={reason}
|
|
onChange={(e) => setReason(e.target.value)}
|
|
placeholder="Expliquez pourquoi ce compte est suspendu. Ce message sera visible par l'utilisateur."
|
|
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-4 text-white focus:outline-none focus:border-red-500 transition-all text-sm shadow-inner"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-6">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 px-4 py-3 bg-slate-800 hover:bg-slate-700 text-white rounded-xl transition-all font-bold text-sm"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="flex-1 px-4 py-3 bg-red-600 hover:bg-red-700 disabled:opacity-50 text-white rounded-xl transition-all font-bold text-sm shadow-lg shadow-red-900/40"
|
|
>
|
|
{isSubmitting ? "En cours..." : "Confirmer"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return createPortal(modalContent, document.body);
|
|
}
|