UI & Feature update - Alpha #9

Merged
Mathis merged 22 commits from dev into prod 2026-01-14 22:40:06 +01:00
9 changed files with 115 additions and 60 deletions
Showing only changes of commit 02d70f27ea - Show all commits

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,7 +12,6 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content"; import type { Category } from "@/types/content";
import { Skeleton } from "@/components/ui/skeleton";
export default function AdminCategoriesPage() { export default function AdminCategoriesPage() {
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
@@ -20,14 +20,16 @@ export default function AdminCategoriesPage() {
useEffect(() => { useEffect(() => {
CategoryService.getAll() CategoryService.getAll()
.then(setCategories) .then(setCategories)
.catch(err => console.error(err)) .catch((err) => console.error(err))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
return ( return (
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8"> <div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">Catégories ({categories.length})</h2> <h2 className="text-3xl font-bold tracking-tight">
Catégories ({categories.length})
</h2>
</div> </div>
<div className="rounded-md border bg-card"> <div className="rounded-md border bg-card">
<Table> <Table>
@@ -41,10 +43,17 @@ export default function AdminCategoriesPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
Array.from({ length: 5 }).map((_, i) => ( Array.from({ length: 5 }).map((_, i) => (
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
<TableRow key={i}> <TableRow key={i}>
<TableCell><Skeleton className="h-4 w-[150px]" /></TableCell> <TableCell>
<TableCell><Skeleton className="h-4 w-[150px]" /></TableCell> <Skeleton className="h-4 w-[150px]" />
<TableCell><Skeleton className="h-4 w-[250px]" /></TableCell> </TableCell>
<TableCell>
<Skeleton className="h-4 w-[150px]" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-[250px]" />
</TableCell>
</TableRow> </TableRow>
)) ))
) : categories.length === 0 ? ( ) : categories.length === 0 ? (
@@ -56,7 +65,9 @@ export default function AdminCategoriesPage() {
) : ( ) : (
categories.map((category) => ( categories.map((category) => (
<TableRow key={category.id}> <TableRow key={category.id}>
<TableCell className="font-medium whitespace-nowrap">{category.name}</TableCell> <TableCell className="font-medium whitespace-nowrap">
{category.name}
</TableCell>
<TableCell className="whitespace-nowrap">{category.slug}</TableCell> <TableCell className="whitespace-nowrap">{category.slug}</TableCell>
<TableCell className="text-muted-foreground"> <TableCell className="text-muted-foreground">
{category.description || "Aucune description"} {category.description || "Aucune description"}

View File

@@ -1,6 +1,12 @@
"use client"; "use client";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
import { Download, Eye, Image as ImageIcon, Trash2, Video } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,12 +17,6 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
import type { Content } from "@/types/content"; 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() { export default function AdminContentsPage() {
const [contents, setContents] = useState<Content[]>([]); const [contents, setContents] = useState<Content[]>([]);
@@ -27,9 +27,9 @@ export default function AdminContentsPage() {
ContentService.getExplore({ limit: 20 }) ContentService.getExplore({ limit: 20 })
.then((res) => { .then((res) => {
setContents(res.data); setContents(res.data);
setTotalCount(res.total); setTotalCount(res.totalCount);
}) })
.catch(err => console.error(err)) .catch((err) => console.error(err))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
@@ -38,8 +38,8 @@ export default function AdminContentsPage() {
try { try {
await ContentService.removeAdmin(id); await ContentService.removeAdmin(id);
setContents(contents.filter(c => c.id !== id)); setContents(contents.filter((c) => c.id !== id));
setTotalCount(prev => prev - 1); setTotalCount((prev) => prev - 1);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -48,7 +48,9 @@ export default function AdminContentsPage() {
return ( return (
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8"> <div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">Contenus ({totalCount})</h2> <h2 className="text-3xl font-bold tracking-tight">
Contenus ({totalCount})
</h2>
</div> </div>
<div className="rounded-md border bg-card"> <div className="rounded-md border bg-card">
<Table> <Table>
@@ -65,12 +67,23 @@ export default function AdminContentsPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
Array.from({ length: 5 }).map((_, i) => ( Array.from({ length: 5 }).map((_, i) => (
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
<TableRow key={i}> <TableRow key={i}>
<TableCell><Skeleton className="h-10 w-[200px]" /></TableCell> <TableCell>
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell> <Skeleton className="h-10 w-[200px]" />
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell> </TableCell>
<TableCell><Skeleton className="h-4 w-[80px]" /></TableCell> <TableCell>
<TableCell><Skeleton className="h-4 w-[100px]" /></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> </TableRow>
)) ))
) : contents.length === 0 ? ( ) : contents.length === 0 ? (
@@ -93,16 +106,18 @@ export default function AdminContentsPage() {
</div> </div>
<div> <div>
<div className="font-semibold">{content.title}</div> <div className="font-semibold">{content.title}</div>
<div className="text-xs text-muted-foreground">{content.type} {content.mimeType}</div> <div className="text-xs text-muted-foreground">
{content.type} {content.mimeType}
</div>
</div> </div>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline">{content.category.name}</Badge> <Badge variant="outline">
</TableCell> {content.category?.name || "Sans catégorie"}
<TableCell> </Badge>
@{content.author.username}
</TableCell> </TableCell>
<TableCell>@{content.author.username}</TableCell>
<TableCell> <TableCell>
<div className="flex flex-col gap-1 text-xs"> <div className="flex flex-col gap-1 text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { AlertCircle, FileText, LayoutGrid, Users } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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"; import { Skeleton } from "@/components/ui/skeleton";
import { type AdminStats, adminService } from "@/services/admin.service";
export default function AdminDashboardPage() { export default function AdminDashboardPage() {
const [stats, setStats] = useState<AdminStats | null>(null); const [stats, setStats] = useState<AdminStats | null>(null);

View File

@@ -1,6 +1,12 @@
"use client"; "use client";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
import { Trash2 } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
Table, Table,
TableBody, TableBody,
@@ -11,12 +17,6 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
import type { User } from "@/types/user"; 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() { export default function AdminUsersPage() {
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
@@ -29,19 +29,24 @@ export default function AdminUsersPage() {
setUsers(res.data); setUsers(res.data);
setTotalCount(res.totalCount); setTotalCount(res.totalCount);
}) })
.catch(err => { .catch((err) => {
console.error(err); console.error(err);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
const handleDelete = async (uuid: string) => { const handleDelete = async (uuid: string) => {
if (!confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.")) return; if (
!confirm(
"Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.",
)
)
return;
try { try {
await UserService.removeUserAdmin(uuid); await UserService.removeUserAdmin(uuid);
setUsers(users.filter(u => u.uuid !== uuid)); setUsers(users.filter((u) => u.uuid !== uuid));
setTotalCount(prev => prev - 1); setTotalCount((prev) => prev - 1);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -50,7 +55,9 @@ export default function AdminUsersPage() {
return ( return (
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8"> <div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">Utilisateurs ({totalCount})</h2> <h2 className="text-3xl font-bold tracking-tight">
Utilisateurs ({totalCount})
</h2>
</div> </div>
<div className="rounded-md border bg-card"> <div className="rounded-md border bg-card">
<Table> <Table>
@@ -67,12 +74,23 @@ export default function AdminUsersPage() {
<TableBody> <TableBody>
{loading ? ( {loading ? (
Array.from({ length: 5 }).map((_, i) => ( Array.from({ length: 5 }).map((_, i) => (
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
<TableRow key={i}> <TableRow key={i}>
<TableCell><Skeleton className="h-4 w-[150px]" /></TableCell> <TableCell>
<TableCell><Skeleton className="h-4 w-[200px]" /></TableCell> <Skeleton className="h-4 w-[150px]" />
<TableCell><Skeleton className="h-4 w-[50px]" /></TableCell> </TableCell>
<TableCell><Skeleton className="h-4 w-[80px]" /></TableCell> <TableCell>
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell> <Skeleton className="h-4 w-[200px]" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-[50px]" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-[80px]" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-[100px]" />
</TableCell>
</TableRow> </TableRow>
)) ))
) : users.length === 0 ? ( ) : users.length === 0 ? (

View File

@@ -1,10 +1,10 @@
import { HelpCircle } from "lucide-react";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import { HelpCircle } from "lucide-react";
export default function HelpPage() { export default function HelpPage() {
const faqs = [ const faqs = [
@@ -48,10 +48,8 @@ export default function HelpPage() {
<h2 className="text-xl font-semibold mb-6">Foire Aux Questions</h2> <h2 className="text-xl font-semibold mb-6">Foire Aux Questions</h2>
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
{faqs.map((faq, index) => ( {faqs.map((faq, index) => (
<AccordionItem key={index} value={`item-${index}`}> <AccordionItem key={faq.question} value={`item-${index}`}>
<AccordionTrigger className="text-left"> <AccordionTrigger className="text-left">{faq.question}</AccordionTrigger>
{faq.question}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground leading-relaxed"> <AccordionContent className="text-muted-foreground leading-relaxed">
{faq.answer} {faq.answer}
</AccordionContent> </AccordionContent>

View File

@@ -4,6 +4,7 @@ import { Calendar, Camera, LogIn, LogOut, Settings } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import * as React from "react"; import * as React from "react";
import { toast } from "sonner";
import { ContentList } from "@/components/content-list"; import { ContentList } from "@/components/content-list";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -20,7 +21,6 @@ import { useAuth } from "@/providers/auth-provider";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service"; import { FavoriteService } from "@/services/favorite.service";
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
import { toast } from "sonner";
export default function ProfilePage() { export default function ProfilePage() {
const { user, isAuthenticated, isLoading, logout, refreshUser } = useAuth(); const { user, isAuthenticated, isLoading, logout, refreshUser } = useAuth();
@@ -32,7 +32,9 @@ export default function ProfilePage() {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file) return; if (!file) return;

View File

@@ -9,7 +9,11 @@ export const metadata: Metadata = {
export default function RecentPage() { export default function RecentPage() {
return ( return (
<React.Suspense fallback={<div className="p-8 text-center">Chargement des nouveautés...</div>}> <React.Suspense
fallback={
<div className="p-8 text-center">Chargement des nouveautés...</div>
}
>
<HomeContent defaultSort="recent" /> <HomeContent defaultSort="recent" />
</React.Suspense> </React.Suspense>
); );

View File

@@ -24,8 +24,8 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { useAuth } from "@/providers/auth-provider"; import { useAuth } from "@/providers/auth-provider";
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
@@ -52,7 +52,7 @@ export default function SettingsPage() {
if (user) { if (user) {
form.reset({ form.reset({
displayName: user.displayName || "", displayName: user.displayName || "",
bio: (user as any).bio || "", bio: user.bio || "",
}); });
} }
}, [user, form]); }, [user, form]);
@@ -107,7 +107,8 @@ export default function SettingsPage() {
<CardHeader> <CardHeader>
<CardTitle>Informations personnelles</CardTitle> <CardTitle>Informations personnelles</CardTitle>
<CardDescription> <CardDescription>
Mettez à jour vos informations publiques. Ces données seront visibles par les autres utilisateurs. Mettez à jour vos informations publiques. Ces données seront visibles par
les autres utilisateurs.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -117,7 +118,11 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Nom d'utilisateur</FormLabel> <FormLabel>Nom d'utilisateur</FormLabel>
<FormControl> <FormControl>
<Input value={user.username} disabled className="bg-zinc-50 dark:bg-zinc-900" /> <Input
value={user.username}
disabled
className="bg-zinc-50 dark:bg-zinc-900"
/>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Le nom d'utilisateur ne peut pas être modifié. Le nom d'utilisateur ne peut pas être modifié.

View File

@@ -9,7 +9,9 @@ export const metadata: Metadata = {
export default function TrendsPage() { export default function TrendsPage() {
return ( return (
<React.Suspense fallback={<div className="p-8 text-center">Chargement des tendances...</div>}> <React.Suspense
fallback={<div className="p-8 text-center">Chargement des tendances...</div>}
>
<HomeContent defaultSort="trend" /> <HomeContent defaultSort="trend" />
</React.Suspense> </React.Suspense>
); );