feat(app): add dashboard pages for settings, admin, and public user profiles
Introduce new pages for profile settings, admin dashboard (users, contents, categories), and public user profiles. Enhance profile functionality with avatar uploads and bio updates. Include help and improved content trends/recent pages. Streamline content display using `HomeContent`.
This commit is contained in:
137
frontend/src/app/(dashboard)/admin/contents/page.tsx
Normal file
137
frontend/src/app/(dashboard)/admin/contents/page.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ContentService } from "@/services/content.service";
|
||||
import type { Content } from "@/types/content";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Eye, Download, Image as ImageIcon, Video, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function AdminContentsPage() {
|
||||
const [contents, setContents] = useState<Content[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
ContentService.getExplore({ limit: 20 })
|
||||
.then((res) => {
|
||||
setContents(res.data);
|
||||
setTotalCount(res.total);
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir supprimer ce contenu ?")) return;
|
||||
|
||||
try {
|
||||
await ContentService.removeAdmin(id);
|
||||
setContents(contents.filter(c => c.id !== id));
|
||||
setTotalCount(prev => prev - 1);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Contenus ({totalCount})</h2>
|
||||
</div>
|
||||
<div className="rounded-md border bg-card">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Contenu</TableHead>
|
||||
<TableHead>Catégorie</TableHead>
|
||||
<TableHead>Auteur</TableHead>
|
||||
<TableHead>Stats</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell><Skeleton className="h-10 w-[200px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-4 w-[80px]" /></TableCell>
|
||||
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : contents.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center h-24">
|
||||
Aucun contenu trouvé.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
contents.map((content) => (
|
||||
<TableRow key={content.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded bg-muted">
|
||||
{content.type === "image" ? (
|
||||
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
||||
) : (
|
||||
<Video className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold">{content.title}</div>
|
||||
<div className="text-xs text-muted-foreground">{content.type} • {content.mimeType}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{content.category.name}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@{content.author.username}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Eye className="h-3 w-3" /> {content.views}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-3 w-3" /> {content.usageCount}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{format(new Date(content.createdAt), "dd/MM/yyyy", { locale: fr })}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(content.id)}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user