diff --git a/frontend/src/app/(dashboard)/admin/categories/page.tsx b/frontend/src/app/(dashboard)/admin/categories/page.tsx new file mode 100644 index 0000000..a1acb88 --- /dev/null +++ b/frontend/src/app/(dashboard)/admin/categories/page.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { CategoryService } from "@/services/category.service"; +import type { Category } from "@/types/content"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AdminCategoriesPage() { + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + CategoryService.getAll() + .then(setCategories) + .catch(err => console.error(err)) + .finally(() => setLoading(false)); + }, []); + + return ( +
+
+

Catégories ({categories.length})

+
+
+ + + + Nom + Slug + Description + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + + + + + )) + ) : categories.length === 0 ? ( + + + Aucune catégorie trouvée. + + + ) : ( + categories.map((category) => ( + + {category.name} + {category.slug} + + {category.description || "Aucune description"} + + + )) + )} + +
+
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/admin/contents/page.tsx b/frontend/src/app/(dashboard)/admin/contents/page.tsx new file mode 100644 index 0000000..f07d495 --- /dev/null +++ b/frontend/src/app/(dashboard)/admin/contents/page.tsx @@ -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([]); + 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 ( +
+
+

Contenus ({totalCount})

+
+
+ + + + Contenu + Catégorie + Auteur + Stats + Date + + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + + + + + + + )) + ) : contents.length === 0 ? ( + + + Aucun contenu trouvé. + + + ) : ( + contents.map((content) => ( + + +
+
+ {content.type === "image" ? ( + + ) : ( +
+
+
{content.title}
+
{content.type} • {content.mimeType}
+
+
+
+ + {content.category.name} + + + @{content.author.username} + + +
+
+ {content.views} +
+
+ {content.usageCount} +
+
+
+ + {format(new Date(content.createdAt), "dd/MM/yyyy", { locale: fr })} + + + + +
+ )) + )} +
+
+
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/admin/page.tsx b/frontend/src/app/(dashboard)/admin/page.tsx new file mode 100644 index 0000000..1043e24 --- /dev/null +++ b/frontend/src/app/(dashboard)/admin/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { adminService, type AdminStats } from "@/services/admin.service"; +import { Users, FileText, LayoutGrid, AlertCircle } from "lucide-react"; +import Link from "next/link"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AdminDashboardPage() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + adminService + .getStats() + .then(setStats) + .catch((err) => { + console.error(err); + setError("Impossible de charger les statistiques."); + }) + .finally(() => setLoading(false)); + }, []); + + if (error) { + return ( +
+ +

{error}

+
+ ); + } + + const statCards = [ + { + title: "Utilisateurs", + value: stats?.users, + icon: Users, + href: "/admin/users", + color: "text-blue-500", + }, + { + title: "Contenus", + value: stats?.contents, + icon: FileText, + href: "/admin/contents", + color: "text-green-500", + }, + { + title: "Catégories", + value: stats?.categories, + icon: LayoutGrid, + href: "/admin/categories", + color: "text-purple-500", + }, + ]; + + return ( +
+
+

Dashboard Admin

+
+
+ {statCards.map((card) => ( + + + + {card.title} + + + + {loading ? ( + + ) : ( +
{card.value}
+ )} +
+
+ + ))} +
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/admin/users/page.tsx b/frontend/src/app/(dashboard)/admin/users/page.tsx new file mode 100644 index 0000000..48df000 --- /dev/null +++ b/frontend/src/app/(dashboard)/admin/users/page.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { UserService } from "@/services/user.service"; +import type { User } from "@/types/user"; +import { Badge } from "@/components/ui/badge"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/ui/button"; +import { Trash2 } from "lucide-react"; + +export default function AdminUsersPage() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [totalCount, setTotalCount] = useState(0); + + useEffect(() => { + UserService.getUsersAdmin() + .then((res) => { + setUsers(res.data); + setTotalCount(res.totalCount); + }) + .catch(err => { + console.error(err); + }) + .finally(() => setLoading(false)); + }, []); + + const handleDelete = async (uuid: string) => { + if (!confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.")) return; + + try { + await UserService.removeUserAdmin(uuid); + setUsers(users.filter(u => u.uuid !== uuid)); + setTotalCount(prev => prev - 1); + } catch (error) { + console.error(error); + } + }; + + return ( +
+
+

Utilisateurs ({totalCount})

+
+
+ + + + Utilisateur + Email + Rôle + Status + Date d'inscription + + + + + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + + + + + + + )) + ) : users.length === 0 ? ( + + + Aucun utilisateur trouvé. + + + ) : ( + users.map((user) => ( + + + {user.displayName || user.username} +
@{user.username}
+
+ {user.email} + + + {user.role} + + + + + {user.status} + + + + {format(new Date(user.createdAt), "PPP", { locale: fr })} + + + + +
+ )) + )} +
+
+
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/help/page.tsx b/frontend/src/app/(dashboard)/help/page.tsx new file mode 100644 index 0000000..53f66b3 --- /dev/null +++ b/frontend/src/app/(dashboard)/help/page.tsx @@ -0,0 +1,72 @@ +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { HelpCircle } from "lucide-react"; + +export default function HelpPage() { + const faqs = [ + { + question: "Comment puis-je publier un mème ?", + answer: + "Pour publier un mème, vous devez être connecté à votre compte. Cliquez sur le bouton 'Publier' dans la barre latérale, choisissez votre fichier (image ou GIF), donnez-lui un titre et une catégorie, puis validez.", + }, + { + question: "Quels formats de fichiers sont acceptés ?", + answer: + "Nous acceptons les images au format PNG, JPEG, WebP et les GIF animés. La taille maximale recommandée est de 2 Mo.", + }, + { + question: "Comment fonctionnent les favoris ?", + answer: + "En cliquant sur l'icône de cœur sur un mème, vous l'ajoutez à vos favoris. Vous pouvez retrouver tous vos mèmes favoris dans l'onglet 'Mes Favoris' de votre profil.", + }, + { + question: "Puis-je supprimer un mème que j'ai publié ?", + answer: + "Oui, vous pouvez supprimer vos propres mèmes en vous rendant sur votre profil, en sélectionnant le mème et en cliquant sur l'option de suppression.", + }, + { + question: "Comment fonctionne le système de recherche ?", + answer: + "Vous pouvez rechercher des mèmes par titre en utilisant la barre de recherche dans la colonne de droite. Vous pouvez également filtrer par catégories ou par tags populaires.", + }, + ]; + + return ( +
+
+
+ +
+

Centre d'aide

+
+ +
+

Foire Aux Questions

+ + {faqs.map((faq, index) => ( + + + {faq.question} + + + {faq.answer} + + + ))} + +
+ +
+

Vous ne trouvez pas de réponse ?

+

+ N'hésitez pas à nous contacter sur nos réseaux sociaux ou par email. +

+

contact@memegoat.local

+
+
+ ); +} diff --git a/frontend/src/app/(dashboard)/profile/page.tsx b/frontend/src/app/(dashboard)/profile/page.tsx index e2f6765..3eda480 100644 --- a/frontend/src/app/(dashboard)/profile/page.tsx +++ b/frontend/src/app/(dashboard)/profile/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Calendar, LogIn, LogOut, Settings } from "lucide-react"; +import { Calendar, Camera, LogIn, LogOut, Settings } from "lucide-react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import * as React from "react"; @@ -19,11 +19,32 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useAuth } from "@/providers/auth-provider"; import { ContentService } from "@/services/content.service"; import { FavoriteService } from "@/services/favorite.service"; +import { UserService } from "@/services/user.service"; +import { toast } from "sonner"; export default function ProfilePage() { - const { user, isAuthenticated, isLoading, logout } = useAuth(); + const { user, isAuthenticated, isLoading, logout, refreshUser } = useAuth(); const searchParams = useSearchParams(); const tab = searchParams.get("tab") || "memes"; + const fileInputRef = React.useRef(null); + + const handleAvatarClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + await UserService.updateAvatar(file); + toast.success("Avatar mis à jour avec succès !"); + await refreshUser?.(); + } catch (error) { + console.error(error); + toast.error("Erreur lors de la mise à jour de l'avatar."); + } + }; const fetchMyMemes = React.useCallback( (params: { limit: number; offset: number }) => @@ -72,12 +93,28 @@ export default function ProfilePage() {
- - - - {user.username.slice(0, 2).toUpperCase()} - - +
+ + + + {user.username.slice(0, 2).toUpperCase()} + + + + +

@@ -85,6 +122,9 @@ export default function ProfilePage() {

@{user.username}

+ {user.bio && ( +

{user.bio}

+ )}
diff --git a/frontend/src/app/(dashboard)/recent/page.tsx b/frontend/src/app/(dashboard)/recent/page.tsx index 328500b..fd5aedc 100644 --- a/frontend/src/app/(dashboard)/recent/page.tsx +++ b/frontend/src/app/(dashboard)/recent/page.tsx @@ -1,15 +1,16 @@ -"use client"; - +import type { Metadata } from "next"; import * as React from "react"; -import { ContentList } from "@/components/content-list"; -import { ContentService } from "@/services/content.service"; +import { HomeContent } from "@/components/home-content"; + +export const metadata: Metadata = { + title: "Nouveautés", + description: "Les tout derniers mèmes fraîchement débarqués sur MemeGoat.", +}; export default function RecentPage() { - const fetchFn = React.useCallback( - (params: { limit: number; offset: number }) => - ContentService.getRecent(params.limit, params.offset), - [], + return ( + Chargement des nouveautés...
}> + + ); - - return ; } diff --git a/frontend/src/app/(dashboard)/settings/page.tsx b/frontend/src/app/(dashboard)/settings/page.tsx new file mode 100644 index 0000000..6a3809d --- /dev/null +++ b/frontend/src/app/(dashboard)/settings/page.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2, Save, User as UserIcon } from "lucide-react"; +import * as React from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import * as z from "zod"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Spinner } from "@/components/ui/spinner"; +import { useAuth } from "@/providers/auth-provider"; +import { UserService } from "@/services/user.service"; + +const settingsSchema = z.object({ + displayName: z.string().max(32, "Le nom d'affichage est trop long").optional(), + bio: z.string().max(255, "La bio est trop longue").optional(), +}); + +type SettingsFormValues = z.infer; + +export default function SettingsPage() { + const { user, isLoading, refreshUser } = useAuth(); + const [isSaving, setIsSaving] = React.useState(false); + + const form = useForm({ + resolver: zodResolver(settingsSchema), + defaultValues: { + displayName: "", + bio: "", + }, + }); + + React.useEffect(() => { + if (user) { + form.reset({ + displayName: user.displayName || "", + bio: (user as any).bio || "", + }); + } + }, [user, form]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!user) { + return ( +
+ + + Accès refusé + + Vous devez être connecté pour accéder aux paramètres. + + + +
+ ); + } + + const onSubmit = async (values: SettingsFormValues) => { + setIsSaving(true); + try { + await UserService.updateMe(values); + toast.success("Paramètres mis à jour !"); + await refreshUser(); + } catch (error) { + console.error(error); + toast.error("Erreur lors de la mise à jour des paramètres."); + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+
+ +
+

Paramètres du profil

+
+ + + + Informations personnelles + + Mettez à jour vos informations publiques. Ces données seront visibles par les autres utilisateurs. + + + +
+ +
+ + Nom d'utilisateur + + + + + Le nom d'utilisateur ne peut pas être modifié. + + + + ( + + Nom d'affichage + + + + + Le nom qui sera affiché sur votre profil et vos mèmes. + + + + )} + /> + + ( + + Bio + +