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:
52
admin/src/app/actions/moderation.ts
Normal file
52
admin/src/app/actions/moderation.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export async function getReports() {
|
||||
return await prisma.messageReport.findMany({
|
||||
include: {
|
||||
message: {
|
||||
include: {
|
||||
sender: {
|
||||
select: { id: true, name: true, email: true }
|
||||
},
|
||||
conversation: {
|
||||
include: {
|
||||
business: {
|
||||
select: { id: true, name: true }
|
||||
},
|
||||
messages: {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
include: {
|
||||
sender: {
|
||||
select: { id: true, name: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
reporter: {
|
||||
select: { id: true, name: true, email: true }
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateReportStatus(id: string, status: 'RESOLVED' | 'DISMISSED') {
|
||||
try {
|
||||
await prisma.messageReport.update({
|
||||
where: { id },
|
||||
data: { status }
|
||||
});
|
||||
revalidatePath('/moderation');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: "Erreur lors de la mise à jour" };
|
||||
}
|
||||
}
|
||||
125
admin/src/app/actions/suspension.ts
Normal file
125
admin/src/app/actions/suspension.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
/**
|
||||
* Suspend a user account
|
||||
*/
|
||||
export async function suspendUser(userId: string, reason: string) {
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
isSuspended: true,
|
||||
suspensionReason: reason,
|
||||
},
|
||||
include: {
|
||||
businesses: true
|
||||
}
|
||||
});
|
||||
|
||||
// If the user is an entrepreneur with a business, we also suspend the business by default
|
||||
// to hide it from the site during the user's suspension.
|
||||
if (user.businesses) {
|
||||
await prisma.business.update({
|
||||
where: { id: user.businesses.id },
|
||||
data: {
|
||||
isSuspended: true,
|
||||
suspensionReason: `Compte propriétaire suspendu : ${reason}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
revalidatePath("/clients");
|
||||
revalidatePath("/entrepreneurs");
|
||||
revalidatePath("/moderation");
|
||||
|
||||
return { success: true, data: user };
|
||||
} catch (error) {
|
||||
console.error("Failed to suspend user:", error);
|
||||
return { success: false, error: "Erreur lors de la suspension" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsuspend a user account
|
||||
*/
|
||||
export async function unsuspendUser(userId: string) {
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
isSuspended: false,
|
||||
suspensionReason: null,
|
||||
},
|
||||
include: {
|
||||
businesses: true
|
||||
}
|
||||
});
|
||||
|
||||
// Also unsuspend their business if they had one
|
||||
if (user.businesses) {
|
||||
await prisma.business.update({
|
||||
where: { id: user.businesses.id },
|
||||
data: {
|
||||
isSuspended: false,
|
||||
suspensionReason: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
revalidatePath("/clients");
|
||||
revalidatePath("/entrepreneurs");
|
||||
revalidatePath("/moderation");
|
||||
|
||||
return { success: true, data: user };
|
||||
} catch (error) {
|
||||
console.error("Failed to unsuspend user:", error);
|
||||
return { success: false, error: "Erreur lors de la réactivation" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a specific business (without suspending the whole user account)
|
||||
*/
|
||||
export async function suspendBusiness(businessId: string, reason: string) {
|
||||
try {
|
||||
const business = await prisma.business.update({
|
||||
where: { id: businessId },
|
||||
data: {
|
||||
isSuspended: true,
|
||||
suspensionReason: reason,
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath("/entrepreneurs");
|
||||
|
||||
return { success: true, data: business };
|
||||
} catch (error) {
|
||||
console.error("Failed to suspend business:", error);
|
||||
return { success: false, error: "Erreur lors de la suspension de la boutique" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsuspend a specific business
|
||||
*/
|
||||
export async function unsuspendBusiness(businessId: string) {
|
||||
try {
|
||||
const business = await prisma.business.update({
|
||||
where: { id: businessId },
|
||||
data: {
|
||||
isSuspended: false,
|
||||
suspensionReason: null,
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath("/entrepreneurs");
|
||||
|
||||
return { success: true, data: business };
|
||||
} catch (error) {
|
||||
console.error("Failed to unsuspend business:", error);
|
||||
return { success: false, error: "Erreur lors de la réactivation de la boutique" };
|
||||
}
|
||||
}
|
||||
27
admin/src/app/actions/user.ts
Normal file
27
admin/src/app/actions/user.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function getClients() {
|
||||
try {
|
||||
const clients = await prisma.user.findMany({
|
||||
where: {
|
||||
role: { not: "ADMIN" },
|
||||
OR: [
|
||||
{ businesses: null },
|
||||
{ businesses: { isActive: false } }
|
||||
]
|
||||
},
|
||||
include: {
|
||||
businesses: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
}
|
||||
});
|
||||
return { success: true, data: clients };
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch clients:", error);
|
||||
return { success: false, error: "Erreur lors de la récupération des clients" };
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { prisma } from "@/lib/prisma";
|
||||
import BlogForm from "@/components/BlogForm";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function EditBlogPage({ params }: { params: { id: string } }) {
|
||||
const { id } = params;
|
||||
export default async function EditBlogPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const post = await prisma.blogPost.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
110
admin/src/app/clients/page.tsx
Normal file
110
admin/src/app/clients/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { getClients } from "@/app/actions/user";
|
||||
import { User, Mail, Phone, MapPin } from "lucide-react";
|
||||
import ClientActions from "@/components/ClientActions";
|
||||
|
||||
export default async function ClientsPage() {
|
||||
const result = await getClients();
|
||||
const clients = result.success ? (result.data || []) : [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Gestion des Clients</h1>
|
||||
<p className="text-slate-400">Liste des utilisateurs n'ayant pas encore de boutique active.</p>
|
||||
</div>
|
||||
<div className="bg-slate-800/50 px-4 py-2 rounded-lg border border-slate-700/50">
|
||||
<span className="text-slate-400 text-sm">Total Clients: </span>
|
||||
<span className="text-indigo-400 font-bold ml-1">{clients.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Utilisateur</th>
|
||||
<th>Contact</th>
|
||||
<th>Localisation</th>
|
||||
<th>Statut</th>
|
||||
<th className="text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{clients.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-12 text-center text-slate-500">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<User className="w-8 h-8 opacity-20" />
|
||||
<p>Aucun client trouvé.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
clients.map((client: any) => (
|
||||
<tr key={client.id} className={`hover:bg-slate-800/30 transition-colors ${client.isSuspended ? 'opacity-60 grayscale-[0.5]' : ''}`}>
|
||||
<td className="py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 font-bold text-sm overflow-hidden border border-slate-700">
|
||||
{client.avatar ? (
|
||||
<img src={client.avatar} alt={client.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
client.name.charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-semibold">{client.name}</p>
|
||||
<p className="text-slate-500 text-xs">ID: {client.id.split('-')[0]}...</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-300">
|
||||
<Mail className="w-3 h-3 text-slate-500" />
|
||||
{client.email}
|
||||
</div>
|
||||
{client.phone && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-300">
|
||||
<Phone className="w-3 h-3 text-slate-500" />
|
||||
{client.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{client.location || "Non spécifié"}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex flex-col gap-1">
|
||||
{client.isSuspended ? (
|
||||
<>
|
||||
<span className="badge badge-error">Compte Suspendu</span>
|
||||
<span className="text-[10px] text-rose-500/70 truncate max-w-[150px]" title={client.suspensionReason}>
|
||||
{client.suspensionReason}
|
||||
</span>
|
||||
</>
|
||||
) : client.businesses ? (
|
||||
<span className="badge badge-pending">Shop Inactif</span>
|
||||
) : (
|
||||
<span className="badge bg-slate-800 text-slate-400">Actif</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<ClientActions userId={client.id} userName={client.name} isSuspended={!!client.isSuspended} />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import ToggleVerifyButton from '@/components/ToggleVerifyButton';
|
||||
import FeaturedModal from '@/components/FeaturedModal';
|
||||
import EntrepreneurActions from '@/components/EntrepreneurActions';
|
||||
import { ShieldAlert, ShieldX } from 'lucide-react';
|
||||
|
||||
async function getBusinesses() {
|
||||
return await prisma.business.findMany({
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
include: {
|
||||
owner: true,
|
||||
},
|
||||
@@ -31,7 +36,7 @@ export default async function EntrepreneursPage() {
|
||||
<th>Catégorie</th>
|
||||
<th>Propriétaire</th>
|
||||
<th>Statut</th>
|
||||
<th className="text-center">Vues</th>
|
||||
<th className="text-center">Visibilité</th>
|
||||
<th className="text-center">Mise en avant</th>
|
||||
<th className="text-right">Action</th>
|
||||
</tr>
|
||||
@@ -39,13 +44,13 @@ export default async function EntrepreneursPage() {
|
||||
<tbody>
|
||||
{businesses.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-10 text-slate-500">
|
||||
<td colSpan={7} className="text-center py-10 text-slate-500">
|
||||
Aucun entrepreneur trouvé.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
businesses.map((business) => (
|
||||
<tr key={business.id}>
|
||||
businesses.map((business: any) => (
|
||||
<tr key={business.id} className={(business.isSuspended || business.owner?.isSuspended) ? 'opacity-60 grayscale-[0.5]' : ''}>
|
||||
<td>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center overflow-hidden">
|
||||
@@ -69,11 +74,21 @@ export default async function EntrepreneursPage() {
|
||||
<div className="text-xs text-slate-500">{business.owner.email}</div>
|
||||
</td>
|
||||
<td>
|
||||
{business.verified ? (
|
||||
<span className="badge badge-verified">Vérifié</span>
|
||||
) : (
|
||||
<span className="badge badge-pending">En attente</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
{business.owner?.isSuspended ? (
|
||||
<span className="badge badge-error flex items-center gap-1">
|
||||
<ShieldX className="w-3 h-3" /> Compte Suspendu
|
||||
</span>
|
||||
) : business.isSuspended ? (
|
||||
<span className="badge bg-orange-500/10 text-orange-500 border border-orange-500/20 flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3" /> Shop Masqué
|
||||
</span>
|
||||
) : business.verified ? (
|
||||
<span className="badge badge-verified">Vérifié</span>
|
||||
) : (
|
||||
<span className="badge badge-pending">En attente</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-center font-medium text-slate-300">
|
||||
{business.viewCount.toLocaleString()}
|
||||
@@ -82,7 +97,18 @@ export default async function EntrepreneursPage() {
|
||||
<FeaturedModal business={business} />
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<ToggleVerifyButton id={business.id} verified={business.verified} />
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<ToggleVerifyButton id={business.id} verified={business.verified} />
|
||||
<div className="h-8 w-[1px] bg-slate-800 mx-1" />
|
||||
<EntrepreneurActions
|
||||
businessId={business.id}
|
||||
businessName={business.name}
|
||||
userId={business.owner.id}
|
||||
userName={business.owner.name}
|
||||
isBusinessSuspended={!!business.isSuspended}
|
||||
isUserSuspended={!!business.owner?.isSuspended}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
@@ -144,6 +144,24 @@ body {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
@@ -161,6 +179,11 @@ body {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Modals */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
@@ -185,11 +208,87 @@ body {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.btn-star {
|
||||
color: var(--muted);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.btn-star:hover, .btn-star.active {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
/* Quill Dark Theme Overrides */
|
||||
.quill-dark-wrapper .ql-toolbar.ql-snow {
|
||||
background-color: var(--secondary);
|
||||
border-color: var(--border);
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-container.ql-snow {
|
||||
background-color: var(--background);
|
||||
border-color: var(--border);
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
min-height: 300px;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-stroke {
|
||||
stroke: var(--foreground);
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-fill {
|
||||
fill: var(--foreground);
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-picker {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-picker-options {
|
||||
background-color: var(--secondary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-picker-item:hover,
|
||||
.quill-dark-wrapper .ql-snow .ql-picker-label:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-active {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow .ql-active .ql-stroke {
|
||||
stroke: var(--primary) !important;
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-editor.ql-blank::before {
|
||||
color: var(--muted);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Styles for the Divider (HR) */
|
||||
.quill-dark-wrapper .ql-editor hr {
|
||||
border: none;
|
||||
border-top: 2px solid var(--border);
|
||||
margin: 1.5em 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow.ql-toolbar button.ql-divider {
|
||||
width: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow.ql-toolbar button.ql-divider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 16px;
|
||||
height: 2px;
|
||||
background-color: var(--foreground);
|
||||
}
|
||||
|
||||
.quill-dark-wrapper .ql-snow.ql-toolbar button.ql-divider:hover::before {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { prisma } from "@/lib/prisma";
|
||||
import InterviewForm from "@/components/InterviewForm";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function EditInterviewPage({ params }: { params: { id: string } }) {
|
||||
const { id } = params;
|
||||
export default async function EditInterviewPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const interview = await prisma.interview.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
135
admin/src/app/moderation/page.tsx
Normal file
135
admin/src/app/moderation/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { getReports } from '@/app/actions/moderation';
|
||||
import ReportActionButtons from '@/components/ReportActionButtons';
|
||||
import ModerationSuspensionButton from '@/components/ModerationSuspensionButton';
|
||||
import { Flag, MessageCircle, User, Calendar } from 'lucide-react';
|
||||
|
||||
export default async function ModerationPage() {
|
||||
const reports = await getReports();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2 flex items-center gap-3">
|
||||
<Flag className="text-red-500" />
|
||||
Modération des Messages
|
||||
</h1>
|
||||
<p className="text-slate-400">Gérer les signalements de contenus inappropriés dans les discussions.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{reports.length === 0 ? (
|
||||
<div className="card p-10 text-center text-slate-500">
|
||||
Aucun signalement en attente.
|
||||
</div>
|
||||
) : (
|
||||
reports.map((report) => (
|
||||
<div key={report.id} className={`card border-l-4 ${report.status === 'PENDING' ? 'border-red-500' : report.status === 'RESOLVED' ? 'border-green-500' : 'border-slate-600'}`}>
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
{/* 1. Report Info */}
|
||||
<div className="md:w-1/3 space-y-4">
|
||||
<div className="flex items-center gap-2 text-slate-400 text-sm">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{new Date(report.createdAt).toLocaleString('fr-FR')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Signalé par</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 font-bold text-xs">
|
||||
{report.reporter.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-white">{report.reporter.name}</div>
|
||||
<div className="text-xs text-slate-500">{report.reporter.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Raison invoquée</div>
|
||||
<p className="text-sm text-slate-300 italic">"{report.reason}"</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<span className={`badge ${
|
||||
report.status === 'PENDING' ? 'badge-pending' :
|
||||
report.status === 'RESOLVED' ? 'badge-verified' :
|
||||
'bg-slate-700 text-slate-300'
|
||||
}`}>
|
||||
{report.status === 'PENDING' ? 'En attente' :
|
||||
report.status === 'RESOLVED' ? 'Résolu' :
|
||||
'Rejeté'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Conversation History */}
|
||||
<div className="flex-1 bg-slate-900/40 rounded-xl border border-slate-800 flex flex-col overflow-hidden">
|
||||
<div className="bg-slate-800/50 p-3 border-b border-slate-700 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-indigo-400">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="text-xs font-bold uppercase tracking-widest text-slate-300">Historique de la discussion</span>
|
||||
</div>
|
||||
{report.message.conversation.business && (
|
||||
<div className="text-[10px] text-slate-500 font-medium">
|
||||
Shop: <span className="text-indigo-400">{report.message.conversation.business.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto max-h-[400px] p-4 space-y-3 bg-slate-950/20">
|
||||
{report.message.conversation.messages.map((msg: any) => {
|
||||
const isReported = msg.id === report.messageId;
|
||||
const senderName = msg.sender.name;
|
||||
|
||||
return (
|
||||
<div key={msg.id} className={`flex flex-col ${isReported ? 'bg-red-500/10 border-red-500/30' : 'bg-slate-800/40 border-slate-700/50'} border rounded-lg p-3 transition-colors`}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={`text-[10px] font-bold uppercase tracking-tighter ${isReported ? 'text-red-400' : 'text-slate-400'}`}>
|
||||
{senderName}
|
||||
</span>
|
||||
<span className="text-[9px] text-slate-500">
|
||||
{new Date(msg.createdAt).toLocaleString('fr-FR', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`text-sm ${isReported ? 'text-white font-medium' : 'text-slate-300'}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
{isReported && (
|
||||
<div className="mt-2 text-[9px] flex items-center gap-1 text-red-500 font-bold uppercase">
|
||||
<Flag className="w-3 h-3" />
|
||||
Message Signalé
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{report.status === 'PENDING' && (
|
||||
<div className="p-4 bg-slate-800/30 border-t border-slate-700">
|
||||
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||||
<div>
|
||||
<div className="mb-3 text-[10px] text-slate-500 font-bold uppercase">Résoudre le signalement</div>
|
||||
<ReportActionButtons reportId={report.id} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 text-[10px] text-slate-500 font-bold uppercase">Sanctionner l'auteur</div>
|
||||
<ModerationSuspensionButton
|
||||
userId={report.message.senderId}
|
||||
userName={report.message.sender.name}
|
||||
isSuspended={!!report.message.sender.isSuspended}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user