diff --git a/frontend/biome.json b/frontend/biome.json index da642c5..ead6448 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -18,6 +18,11 @@ "enabled": true, "rules": { "recommended": true, + "a11y": { + "useAriaPropsForRole": "warn", + "useSemanticElements": "warn", + "useFocusableInteractive": "warn" + }, "suspicious": { "noUnknownAtRules": "off" } diff --git a/frontend/components.json b/frontend/components.json index dec871e..9accbea 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -1,22 +1,22 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "stone", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "registries": {} + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} } diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 6f0ecfd..42d5ca0 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -1,124 +1,128 @@ "use client"; -import * as React from "react"; -import Link from "next/link"; -import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowLeft } from "lucide-react"; +import Link from "next/link"; +import * as React from "react"; +import { useForm } from "react-hook-form"; import * as z from "zod"; -import { useAuth } from "@/providers/auth-provider"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; -import { ArrowLeft } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { useAuth } from "@/providers/auth-provider"; const loginSchema = z.object({ - email: z.string().email({ message: "Email invalide" }), - password: z.string().min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }), + email: z.string().email({ message: "Email invalide" }), + password: z + .string() + .min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }), }); type LoginFormValues = z.infer; export default function LoginPage() { - const { login } = useAuth(); - const [loading, setLoading] = React.useState(false); + const { login } = useAuth(); + const [loading, setLoading] = React.useState(false); - const form = useForm({ - resolver: zodResolver(loginSchema), - defaultValues: { - email: "", - password: "", - }, - }); + const form = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + email: "", + password: "", + }, + }); - async function onSubmit(values: LoginFormValues) { - setLoading(true); - try { - await login(values.email, values.password); - } catch (error) { - // Error is handled in useAuth via toast - } finally { - setLoading(false); - } - } + async function onSubmit(values: LoginFormValues) { + setLoading(true); + try { + await login(values.email, values.password); + } catch (_error) { + // Error is handled in useAuth via toast + } finally { + setLoading(false); + } + } - return ( -
-
- - - Retour à l'accueil - - - - Connexion - - Entrez vos identifiants pour accéder à votre compte MemeGoat. - - - -
- - ( - - Email - - - - - - )} - /> - ( - - Mot de passe - - - - - - )} - /> - - - -
- -

- Vous n'avez pas de compte ?{" "} - - S'inscrire - -

-
-
-
-
- ); + return ( +
+
+ + + Retour à l'accueil + + + + Connexion + + Entrez vos identifiants pour accéder à votre compte MemeGoat. + + + +
+ + ( + + Email + + + + + + )} + /> + ( + + Mot de passe + + + + + + )} + /> + + + +
+ +

+ Vous n'avez pas de compte ?{" "} + + S'inscrire + +

+
+
+
+
+ ); } diff --git a/frontend/src/app/(auth)/register/page.tsx b/frontend/src/app/(auth)/register/page.tsx index 4dc4370..1a52bb1 100644 --- a/frontend/src/app/(auth)/register/page.tsx +++ b/frontend/src/app/(auth)/register/page.tsx @@ -1,153 +1,157 @@ "use client"; -import * as React from "react"; -import Link from "next/link"; -import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { ArrowLeft } from "lucide-react"; +import Link from "next/link"; +import * as React from "react"; +import { useForm } from "react-hook-form"; import * as z from "zod"; -import { useAuth } from "@/providers/auth-provider"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; -import { ArrowLeft } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { useAuth } from "@/providers/auth-provider"; const registerSchema = z.object({ - username: z.string().min(3, { message: "Le pseudo doit faire au moins 3 caractères" }), - email: z.string().email({ message: "Email invalide" }), - password: z.string().min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }), - displayName: z.string().optional(), + username: z + .string() + .min(3, { message: "Le pseudo doit faire au moins 3 caractères" }), + email: z.string().email({ message: "Email invalide" }), + password: z + .string() + .min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }), + displayName: z.string().optional(), }); type RegisterFormValues = z.infer; export default function RegisterPage() { - const { register } = useAuth(); - const [loading, setLoading] = React.useState(false); + const { register } = useAuth(); + const [loading, setLoading] = React.useState(false); - const form = useForm({ - resolver: zodResolver(registerSchema), - defaultValues: { - username: "", - email: "", - password: "", - displayName: "", - }, - }); + const form = useForm({ + resolver: zodResolver(registerSchema), + defaultValues: { + username: "", + email: "", + password: "", + displayName: "", + }, + }); - async function onSubmit(values: RegisterFormValues) { - setLoading(true); - try { - await register(values); - } catch (error) { - // Error handled in useAuth - } finally { - setLoading(false); - } - } + async function onSubmit(values: RegisterFormValues) { + setLoading(true); + try { + await register(values); + } catch (_error) { + // Error handled in useAuth + } finally { + setLoading(false); + } + } - return ( -
-
- - - Retour à l'accueil - - - - Inscription - - Rejoignez la communauté MemeGoat dès aujourd'hui. - - - -
- - ( - - Pseudo - - - - - - )} - /> - ( - - Email - - - - - - )} - /> - ( - - Nom d'affichage (Optionnel) - - - - - - )} - /> - ( - - Mot de passe - - - - - - )} - /> - - - -
- -

- Vous avez déjà un compte ?{" "} - - Se connecter - -

-
-
-
-
- ); + return ( +
+
+ + + Retour à l'accueil + + + + Inscription + + Rejoignez la communauté MemeGoat dès aujourd'hui. + + + +
+ + ( + + Pseudo + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Nom d'affichage (Optionnel) + + + + + + )} + /> + ( + + Mot de passe + + + + + + )} + /> + + + +
+ +

+ Vous avez déjà un compte ?{" "} + + Se connecter + +

+
+
+
+
+ ); } diff --git a/frontend/src/app/(dashboard)/@modal/(.)meme/[slug]/page.tsx b/frontend/src/app/(dashboard)/@modal/(.)meme/[slug]/page.tsx index 9b6d733..6645116 100644 --- a/frontend/src/app/(dashboard)/@modal/(.)meme/[slug]/page.tsx +++ b/frontend/src/app/(dashboard)/@modal/(.)meme/[slug]/page.tsx @@ -1,45 +1,58 @@ "use client"; -import * as React from "react"; import { useRouter } from "next/navigation"; -import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"; -import { ContentService } from "@/services/content.service"; +import * as React from "react"; import { ContentCard } from "@/components/content-card"; -import type { Content } from "@/types/content"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; +import { ContentService } from "@/services/content.service"; +import type { Content } from "@/types/content"; -export default function MemeModal({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = React.use(params); - const router = useRouter(); - const [content, setContent] = React.useState(null); - const [loading, setLoading] = React.useState(true); +export default function MemeModal({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = React.use(params); + const router = useRouter(); + const [content, setContent] = React.useState(null); + const [loading, setLoading] = React.useState(true); - React.useEffect(() => { - ContentService.getOne(slug) - .then(setContent) - .catch(console.error) - .finally(() => setLoading(false)); - }, [slug]); + React.useEffect(() => { + ContentService.getOne(slug) + .then(setContent) + .catch(console.error) + .finally(() => setLoading(false)); + }, [slug]); - return ( - !open && router.back()}> - - {content?.title || "Détail du mème"} - Affiche le mème en grand avec ses détails - {loading ? ( -
- -
- ) : content ? ( -
- -
- ) : ( -
-

Impossible de charger ce mème.

-
- )} -
-
- ); + return ( + !open && router.back()}> + + + {content?.title || "Détail du mème"} + + + Affiche le mème en grand avec ses détails + + {loading ? ( +
+ +
+ ) : content ? ( +
+ +
+ ) : ( +
+

Impossible de charger ce mème.

+
+ )} +
+
+ ); } diff --git a/frontend/src/app/(dashboard)/@modal/default.tsx b/frontend/src/app/(dashboard)/@modal/default.tsx index 6ddf1b7..3e4bcd3 100644 --- a/frontend/src/app/(dashboard)/@modal/default.tsx +++ b/frontend/src/app/(dashboard)/@modal/default.tsx @@ -1,3 +1,3 @@ export default function Default() { - return null; + return null; } diff --git a/frontend/src/app/(dashboard)/_hooks/use-infinite-scroll.ts b/frontend/src/app/(dashboard)/_hooks/use-infinite-scroll.ts index eef3110..6405d8e 100644 --- a/frontend/src/app/(dashboard)/_hooks/use-infinite-scroll.ts +++ b/frontend/src/app/(dashboard)/_hooks/use-infinite-scroll.ts @@ -1,42 +1,42 @@ import * as React from "react"; interface UseInfiniteScrollOptions { - threshold?: number; - hasMore: boolean; - loading: boolean; - onLoadMore: () => void; + threshold?: number; + hasMore: boolean; + loading: boolean; + onLoadMore: () => void; } export function useInfiniteScroll({ - threshold = 1.0, - hasMore, - loading, - onLoadMore, + threshold = 1.0, + hasMore, + loading, + onLoadMore, }: UseInfiniteScrollOptions) { - const loaderRef = React.useRef(null); + const loaderRef = React.useRef(null); - React.useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !loading) { - onLoadMore(); - } - }, - { threshold } - ); + React.useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading) { + onLoadMore(); + } + }, + { threshold }, + ); - const currentLoader = loaderRef.current; - if (currentLoader) { - observer.observe(currentLoader); - } + const currentLoader = loaderRef.current; + if (currentLoader) { + observer.observe(currentLoader); + } - return () => { - if (currentLoader) { - observer.unobserve(currentLoader); - } - observer.disconnect(); - }; - }, [onLoadMore, hasMore, loading, threshold]); + return () => { + if (currentLoader) { + observer.unobserve(currentLoader); + } + observer.disconnect(); + }; + }, [onLoadMore, hasMore, loading, threshold]); - return { loaderRef }; + return { loaderRef }; } diff --git a/frontend/src/app/(dashboard)/category/[slug]/page.tsx b/frontend/src/app/(dashboard)/category/[slug]/page.tsx index e7b44bd..f11753f 100644 --- a/frontend/src/app/(dashboard)/category/[slug]/page.tsx +++ b/frontend/src/app/(dashboard)/category/[slug]/page.tsx @@ -1,31 +1,30 @@ -import * as React from "react"; import type { Metadata } from "next"; import { CategoryContent } from "@/components/category-content"; import { CategoryService } from "@/services/category.service"; -export async function generateMetadata({ - params -}: { - params: Promise<{ slug: string }> +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; }): Promise { - const { slug } = await params; - try { - const categories = await CategoryService.getAll(); - const category = categories.find(c => c.slug === slug); - return { - title: `${category?.name || slug} | MemeGoat`, - description: `Découvrez tous les mèmes de la catégorie ${category?.name || slug} sur MemeGoat.`, - }; - } catch (error) { - return { title: `Catégorie : ${slug} | MemeGoat` }; - } + const { slug } = await params; + try { + const categories = await CategoryService.getAll(); + const category = categories.find((c) => c.slug === slug); + return { + title: `${category?.name || slug} | MemeGoat`, + description: `Découvrez tous les mèmes de la catégorie ${category?.name || slug} sur MemeGoat.`, + }; + } catch (_error) { + return { title: `Catégorie : ${slug} | MemeGoat` }; + } } -export default async function CategoryPage({ - params -}: { - params: Promise<{ slug: string }> +export default async function CategoryPage({ + params, +}: { + params: Promise<{ slug: string }>; }) { - const { slug } = await params; - return ; + const { slug } = await params; + return ; } diff --git a/frontend/src/app/(dashboard)/category/page.tsx b/frontend/src/app/(dashboard)/category/page.tsx index 94a5378..e97d510 100644 --- a/frontend/src/app/(dashboard)/category/page.tsx +++ b/frontend/src/app/(dashboard)/category/page.tsx @@ -1,54 +1,54 @@ "use client"; -import * as React from "react"; +import { LayoutGrid } from "lucide-react"; import Link from "next/link"; +import * as React from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CategoryService } from "@/services/category.service"; import type { Category } from "@/types/content"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { LayoutGrid } from "lucide-react"; export default function CategoriesPage() { - const [categories, setCategories] = React.useState([]); - const [loading, setLoading] = React.useState(true); + const [categories, setCategories] = React.useState([]); + const [loading, setLoading] = React.useState(true); - React.useEffect(() => { - CategoryService.getAll() - .then(setCategories) - .finally(() => setLoading(false)); - }, []); + React.useEffect(() => { + CategoryService.getAll() + .then(setCategories) + .finally(() => setLoading(false)); + }, []); - return ( -
-
- -

Catégories

-
+ return ( +
+
+ +

Catégories

+
-
- {loading ? ( - Array.from({ length: 6 }).map((_, i) => ( - - - - - )) - ) : ( - categories.map((category) => ( - - - - {category.name} - - -

- {category.description || `Découvrez tous les mèmes de la catégorie ${category.name}.`} -

-
-
- - )) - )} -
-
- ); +
+ {loading + ? Array.from({ length: 6 }).map((_, i) => ( + /* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */ + + + + + )) + : categories.map((category) => ( + + + + {category.name} + + +

+ {category.description || + `Découvrez tous les mèmes de la catégorie ${category.name}.`} +

+
+
+ + ))} +
+
+ ); } diff --git a/frontend/src/app/(dashboard)/layout.tsx b/frontend/src/app/(dashboard)/layout.tsx index 792cba6..181c20b 100644 --- a/frontend/src/app/(dashboard)/layout.tsx +++ b/frontend/src/app/(dashboard)/layout.tsx @@ -1,37 +1,41 @@ import * as React from "react"; -import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar"; import { AppSidebar } from "@/components/app-sidebar"; -import { SearchSidebar } from "@/components/search-sidebar"; import { MobileFilters } from "@/components/mobile-filters"; +import { SearchSidebar } from "@/components/search-sidebar"; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar"; export default function DashboardLayout({ - children, - modal, + children, + modal, }: { - children: React.ReactNode; - modal: React.ReactNode; + children: React.ReactNode; + modal: React.ReactNode; }) { - return ( - - - -
-
- -
-
-
- {children} - {modal} -
- - - -
- - - -
-
- ); + return ( + + + +
+
+ +
+
+
+ {children} + {modal} +
+ + + +
+ + + +
+
+ ); } diff --git a/frontend/src/app/(dashboard)/loading.tsx b/frontend/src/app/(dashboard)/loading.tsx index 8d18edb..e373722 100644 --- a/frontend/src/app/(dashboard)/loading.tsx +++ b/frontend/src/app/(dashboard)/loading.tsx @@ -1,13 +1,14 @@ import { ContentSkeleton } from "@/components/content-skeleton"; export default function Loading() { - return ( -
-
- {[...Array(3)].map((_, i) => ( - - ))} -
-
- ); + return ( +
+
+ {[...Array(3)].map((_, i) => ( + /* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */ + + ))} +
+
+ ); } diff --git a/frontend/src/app/(dashboard)/meme/[slug]/page.tsx b/frontend/src/app/(dashboard)/meme/[slug]/page.tsx index 8cba663..1c978f6 100644 --- a/frontend/src/app/(dashboard)/meme/[slug]/page.tsx +++ b/frontend/src/app/(dashboard)/meme/[slug]/page.tsx @@ -1,90 +1,98 @@ -import * as React from "react"; -import type { Metadata } from "next"; -import { ContentService } from "@/services/content.service"; -import { ContentCard } from "@/components/content-card"; -import { Button } from "@/components/ui/button"; import { ChevronLeft } from "lucide-react"; +import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; +import { ContentCard } from "@/components/content-card"; +import { Button } from "@/components/ui/button"; +import { ContentService } from "@/services/content.service"; export const revalidate = 3600; // ISR: Revalider toutes les heures -export async function generateMetadata({ - params -}: { - params: Promise<{ slug: string }> +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; }): Promise { - const { slug } = await params; - try { - const content = await ContentService.getOne(slug); - return { - title: `${content.title} | MemeGoat`, - description: content.description || `Regardez ce mème : ${content.title}`, - openGraph: { - images: [content.thumbnailUrl || content.url], - }, - }; - } catch (error) { - return { title: "Mème non trouvé | MemeGoat" }; - } + const { slug } = await params; + try { + const content = await ContentService.getOne(slug); + return { + title: `${content.title} | MemeGoat`, + description: content.description || `Regardez ce mème : ${content.title}`, + openGraph: { + images: [content.thumbnailUrl || content.url], + }, + }; + } catch (_error) { + return { title: "Mème non trouvé | MemeGoat" }; + } } -export default async function MemePage({ - params -}: { - params: Promise<{ slug: string }> +export default async function MemePage({ + params, +}: { + params: Promise<{ slug: string }>; }) { - const { slug } = await params; - - try { - const content = await ContentService.getOne(slug); - - return ( -
- - - Retour au flux - - -
-
- -
- -
-
-

À propos de ce mème

-
-
-

Publié par

-

{content.author.displayName || content.author.username}

-
-
-

Date

-

{new Date(content.createdAt).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'long', - year: 'numeric' - })}

-
- {content.description && ( -
-

Description

-

{content.description}

-
- )} -
-
- -
-

Envie de créer votre propre mème ?

- -
-
-
-
- ); - } catch (error) { - notFound(); - } + const { slug } = await params; + + try { + const content = await ContentService.getOne(slug); + + return ( +
+ + + Retour au flux + + +
+
+ +
+ +
+
+

À propos de ce mème

+
+
+

Publié par

+

+ {content.author.displayName || content.author.username} +

+
+
+

Date

+

+ {new Date(content.createdAt).toLocaleDateString("fr-FR", { + day: "numeric", + month: "long", + year: "numeric", + })} +

+
+ {content.description && ( +
+

Description

+

{content.description}

+
+ )} +
+
+ +
+

+ Envie de créer votre propre mème ? +

+ +
+
+
+
+ ); + } catch (_error) { + notFound(); + } } diff --git a/frontend/src/app/(dashboard)/page.tsx b/frontend/src/app/(dashboard)/page.tsx index cf4b60c..ea7fabd 100644 --- a/frontend/src/app/(dashboard)/page.tsx +++ b/frontend/src/app/(dashboard)/page.tsx @@ -1,21 +1,24 @@ -import * as React from "react"; import type { Metadata } from "next"; +import * as React from "react"; import { HomeContent } from "@/components/home-content"; import { Spinner } from "@/components/ui/spinner"; export const metadata: Metadata = { - title: "MemeGoat | La meilleure plateforme de mèmes pour les chèvres", - description: "Explorez, créez et partagez les meilleurs mèmes de la communauté. Rejoignez le troupeau sur MemeGoat.", + title: "MemeGoat | La meilleure plateforme de mèmes pour les chèvres", + description: + "Explorez, créez et partagez les meilleurs mèmes de la communauté. Rejoignez le troupeau sur MemeGoat.", }; export default function HomePage() { - return ( - - - - }> - - - ); + return ( + + + + } + > + + + ); } diff --git a/frontend/src/app/(dashboard)/profile/page.tsx b/frontend/src/app/(dashboard)/profile/page.tsx index 14d09fa..cc02784 100644 --- a/frontend/src/app/(dashboard)/profile/page.tsx +++ b/frontend/src/app/(dashboard)/profile/page.tsx @@ -1,82 +1,96 @@ "use client"; -import * as React from "react"; -import { useAuth } from "@/providers/auth-provider"; -import { ContentList } from "@/components/content-list"; -import { ContentService } from "@/services/content.service"; -import { FavoriteService } from "@/services/favorite.service"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button"; -import { Settings, LogOut, Calendar } from "lucide-react"; +import { Calendar, LogOut, Settings } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; +import * as React from "react"; +import { ContentList } from "@/components/content-list"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +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"; export default function ProfilePage() { - const { user, isAuthenticated, isLoading, logout } = useAuth(); + const { user, isAuthenticated, isLoading, logout } = useAuth(); - if (isLoading) return null; - if (!isAuthenticated || !user) { - redirect("/login"); - } + const fetchMyMemes = React.useCallback( + (params: { limit: number; offset: number }) => + ContentService.getExplore({ ...params, author: user?.username }), + [user?.username], + ); - const fetchMyMemes = React.useCallback((params: { limit: number; offset: number }) => - ContentService.getExplore({ ...params, author: user.username }), - [user.username]); + const fetchMyFavorites = React.useCallback( + (params: { limit: number; offset: number }) => FavoriteService.list(params), + [], + ); - const fetchMyFavorites = React.useCallback((params: { limit: number; offset: number }) => - FavoriteService.list(params), - []); + if (isLoading) return null; + if (!isAuthenticated || !user) { + redirect("/login"); + } - return ( -
-
-
- - - - {user.username.slice(0, 2).toUpperCase()} - - -
-
-

{user.displayName || user.username}

-

@{user.username}

-
-
- - - Membre depuis {new Date(user.createdAt).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })} - -
-
- - -
-
-
-
+ return ( +
+
+
+ + + + {user.username.slice(0, 2).toUpperCase()} + + +
+
+

+ {user.displayName || user.username} +

+

@{user.username}

+
+
+ + + Membre depuis{" "} + {new Date(user.createdAt).toLocaleDateString("fr-FR", { + month: "long", + year: "numeric", + })} + +
+
+ + +
+
+
+
- - - Mes Mèmes - Mes Favoris - - - - - - - - -
- ); + + + Mes Mèmes + Mes Favoris + + + + + + + + +
+ ); } diff --git a/frontend/src/app/(dashboard)/recent/page.tsx b/frontend/src/app/(dashboard)/recent/page.tsx index 46dcaaa..328500b 100644 --- a/frontend/src/app/(dashboard)/recent/page.tsx +++ b/frontend/src/app/(dashboard)/recent/page.tsx @@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list"; import { ContentService } from "@/services/content.service"; export default function RecentPage() { - const fetchFn = React.useCallback((params: { limit: number; offset: number }) => - ContentService.getRecent(params.limit, params.offset), - []); + const fetchFn = React.useCallback( + (params: { limit: number; offset: number }) => + ContentService.getRecent(params.limit, params.offset), + [], + ); - return ; + return ; } diff --git a/frontend/src/app/(dashboard)/trends/page.tsx b/frontend/src/app/(dashboard)/trends/page.tsx index 6e69a4d..7ffdc4d 100644 --- a/frontend/src/app/(dashboard)/trends/page.tsx +++ b/frontend/src/app/(dashboard)/trends/page.tsx @@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list"; import { ContentService } from "@/services/content.service"; export default function TrendsPage() { - const fetchFn = React.useCallback((params: { limit: number; offset: number }) => - ContentService.getTrends(params.limit, params.offset), - []); + const fetchFn = React.useCallback( + (params: { limit: number; offset: number }) => + ContentService.getTrends(params.limit, params.offset), + [], + ); - return ; + return ; } diff --git a/frontend/src/app/(dashboard)/upload/page.tsx b/frontend/src/app/(dashboard)/upload/page.tsx index 27a18b5..2567fbf 100644 --- a/frontend/src/app/(dashboard)/upload/page.tsx +++ b/frontend/src/app/(dashboard)/upload/page.tsx @@ -1,257 +1,283 @@ "use client"; -import * as React from "react"; -import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import * as z from "zod"; -import { Upload, Image as ImageIcon, Film, X, Loader2 } from "lucide-react"; +import { Image as ImageIcon, Loader2, Upload, X } from "lucide-react"; +import NextImage from "next/image"; +import { useRouter } from "next/navigation"; +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, CardHeader, CardTitle } from "@/components/ui/card"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CategoryService } from "@/services/category.service"; import { ContentService } from "@/services/content.service"; import type { Category } from "@/types/content"; const uploadSchema = z.object({ - title: z.string().min(3, "Le titre doit faire au moins 3 caractères"), - type: z.enum(["meme", "gif"]), - categoryId: z.string().optional(), - tags: z.string().optional(), + title: z.string().min(3, "Le titre doit faire au moins 3 caractères"), + type: z.enum(["meme", "gif"]), + categoryId: z.string().optional(), + tags: z.string().optional(), }); type UploadFormValues = z.infer; export default function UploadPage() { - const router = useRouter(); - const [categories, setCategories] = React.useState([]); - const [file, setFile] = React.useState(null); - const [preview, setPreview] = React.useState(null); - const [isUploading, setIsUploading] = React.useState(false); + const router = useRouter(); + const [categories, setCategories] = React.useState([]); + const [file, setFile] = React.useState(null); + const [preview, setPreview] = React.useState(null); + const [isUploading, setIsUploading] = React.useState(false); - const form = useForm({ - resolver: zodResolver(uploadSchema), - defaultValues: { - title: "", - type: "meme", - tags: "", - }, - }); + const form = useForm({ + resolver: zodResolver(uploadSchema), + defaultValues: { + title: "", + type: "meme", + tags: "", + }, + }); - React.useEffect(() => { - CategoryService.getAll().then(setCategories).catch(console.error); - }, []); + React.useEffect(() => { + CategoryService.getAll().then(setCategories).catch(console.error); + }, []); - const handleFileChange = (e: React.ChangeEvent) => { - const selectedFile = e.target.files?.[0]; - if (selectedFile) { - if (selectedFile.size > 10 * 1024 * 1024) { - toast.error("Le fichier est trop volumineux (max 10Mo)"); - return; - } - setFile(selectedFile); - const reader = new FileReader(); - reader.onloadend = () => { - setPreview(reader.result as string); - }; - reader.readAsDataURL(selectedFile); - } - }; + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFile = e.target.files?.[0]; + if (selectedFile) { + if (selectedFile.size > 10 * 1024 * 1024) { + toast.error("Le fichier est trop volumineux (max 10Mo)"); + return; + } + setFile(selectedFile); + const reader = new FileReader(); + reader.onloadend = () => { + setPreview(reader.result as string); + }; + reader.readAsDataURL(selectedFile); + } + }; - const onSubmit = async (values: UploadFormValues) => { - if (!file) { - toast.error("Veuillez sélectionner un fichier"); - return; - } + const onSubmit = async (values: UploadFormValues) => { + if (!file) { + toast.error("Veuillez sélectionner un fichier"); + return; + } - setIsUploading(true); - try { - const formData = new FormData(); - formData.append("file", file); - formData.append("title", values.title); - formData.append("type", values.type); - if (values.categoryId) formData.append("categoryId", values.categoryId); - if (values.tags) { - const tagsArray = values.tags.split(",").map(t => t.trim()).filter(t => t !== ""); - tagsArray.forEach(tag => formData.append("tags[]", tag)); - } + setIsUploading(true); + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("title", values.title); + formData.append("type", values.type); + if (values.categoryId) formData.append("categoryId", values.categoryId); + if (values.tags) { + const tagsArray = values.tags + .split(",") + .map((t) => t.trim()) + .filter((t) => t !== ""); + for (const tag of tagsArray) { + formData.append("tags[]", tag); + } + } - await ContentService.upload(formData); - toast.success("Mème uploadé avec succès !"); - router.push("/"); - } catch (error: any) { - console.error("Upload failed:", error); - toast.error(error.response?.data?.message || "Échec de l'upload. Êtes-vous connecté ?"); - } finally { - setIsUploading(false); - } - }; + await ContentService.upload(formData); + toast.success("Mème uploadé avec succès !"); + router.push("/"); + } catch (error: unknown) { + console.error("Upload failed:", error); + let errorMessage = "Échec de l'upload. Êtes-vous connecté ?"; + if ( + error && + typeof error === "object" && + "response" in error && + error.response && + typeof error.response === "object" && + "data" in error.response && + error.response.data && + typeof error.response.data === "object" && + "message" in error.response.data && + typeof error.response.data.message === "string" + ) { + errorMessage = error.response.data.message; + } + toast.error(errorMessage); + } finally { + setIsUploading(false); + } + }; - return ( -
- - - - - Partager un mème - - - -
- -
- Fichier (Image ou GIF) - {!preview ? ( -
document.getElementById("file-upload")?.click()} - > -
- -
-

Cliquez pour choisir un fichier

-

PNG, JPG ou GIF jusqu'à 10Mo

- -
- ) : ( -
- Preview - -
- )} -
+ return ( +
+ + + + + Partager un mème + + + + + +
+ Fichier (Image ou GIF) + {!preview ? ( + + ) : ( +
+
+ +
+ +
+ )} +
- ( - - Titre - - - - - - )} - /> + ( + + Titre + + + + + + )} + /> -
- ( - - Format - - - - )} - /> +
+ ( + + Format + + + + )} + /> - ( - - Catégorie - - - - )} - /> -
+ ( + + Catégorie + + + + )} + /> +
- ( - - Tags - - - - - Séparez les tags par des virgules. - - - - )} - /> + ( + + Tags + + + + Séparez les tags par des virgules. + + + )} + /> - - - -
-
-
- ); + + + +
+
+
+ ); } diff --git a/frontend/src/app/error.tsx b/frontend/src/app/error.tsx index 6b6e2ba..48fb2d4 100644 --- a/frontend/src/app/error.tsx +++ b/frontend/src/app/error.tsx @@ -1,46 +1,50 @@ "use client"; +import { AlertTriangle, Home, RefreshCw } from "lucide-react"; +import Link from "next/link"; import { useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { AlertTriangle, RefreshCw, Home } from "lucide-react"; -import Link from "next/link"; +// biome-ignore lint/suspicious/noShadowRestrictedNames: correct use export default function Error({ - error, - reset, + error, + reset, }: { - error: Error & { digest?: string }; - reset: () => void; + error: Error & { digest?: string }; + reset: () => void; }) { - useEffect(() => { - console.error(error); - }, [error]); + useEffect(() => { + console.error(error); + }, [error]); - return ( -
-
-
-
- -
-
-

Oups ! Une erreur est survenue

-

- La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur pied. -

-
- - -
-
-
- ); + return ( +
+
+
+
+ +
+
+

+ Oups ! Une erreur est survenue +

+

+ La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur + pied. +

+
+ + +
+
+
+ ); } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 04ab0a9..2e62e75 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -6,8 +6,11 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-sans: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -80,34 +83,37 @@ --popover: oklch(0.9911 0 0); --popover-foreground: oklch(0.2435 0 0); --primary: oklch(0.4341 0.0392 41.9938); - --primary-foreground: oklch(1.0000 0 0); - --secondary: oklch(0.9200 0.0651 74.3695); + --primary-foreground: oklch(1 0 0); + --secondary: oklch(0.92 0.0651 74.3695); --secondary-foreground: oklch(0.3499 0.0685 40.8288); --muted: oklch(0.9521 0 0); --muted-foreground: oklch(0.5032 0 0); - --accent: oklch(0.9310 0 0); + --accent: oklch(0.931 0 0); --accent-foreground: oklch(0.2435 0 0); - --destructive: oklch(0.6271 0.1936 33.3390); + --destructive: oklch(0.6271 0.1936 33.339); --border: oklch(0.8822 0 0); --input: oklch(0.8822 0 0); --ring: oklch(0.4341 0.0392 41.9938); --chart-1: oklch(0.4341 0.0392 41.9938); - --chart-2: oklch(0.9200 0.0651 74.3695); - --chart-3: oklch(0.9310 0 0); + --chart-2: oklch(0.92 0.0651 74.3695); + --chart-3: oklch(0.931 0 0); --chart-4: oklch(0.9367 0.0523 75.5009); --chart-5: oklch(0.4338 0.0437 41.6746); --sidebar: oklch(0.9881 0 0); --sidebar-foreground: oklch(0.2645 0 0); - --sidebar-primary: oklch(0.3250 0 0); + --sidebar-primary: oklch(0.325 0 0); --sidebar-primary-foreground: oklch(0.9881 0 0); --sidebar-accent: oklch(0.9761 0 0); - --sidebar-accent-foreground: oklch(0.3250 0 0); + --sidebar-accent-foreground: oklch(0.325 0 0); --sidebar-border: oklch(0.9401 0 0); --sidebar-ring: oklch(0.7731 0 0); - --destructive-foreground: oklch(1.0000 0 0); - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --destructive-foreground: oklch(1 0 0); + --font-sans: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --shadow-color: oklch(0 0 0); --shadow-opacity: 0.09; --shadow-blur: 5.5px; @@ -118,11 +124,21 @@ --spacing: 0.2rem; --shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); --shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); - --shadow-sm: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); - --shadow: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); - --shadow-md: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09); - --shadow-lg: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09); - --shadow-xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09); + --shadow-sm: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); + --shadow: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); + --shadow-md: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09); + --shadow-lg: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09); + --shadow-xl: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09); --shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22); --tracking-normal: 0em; } @@ -135,35 +151,38 @@ --popover: oklch(0.2134 0 0); --popover-foreground: oklch(0.9491 0 0); --primary: oklch(0.9247 0.0524 66.1732); - --primary-foreground: oklch(0.2490 0.0317 198.7326); - --secondary: oklch(0.3163 0.0190 63.6992); + --primary-foreground: oklch(0.249 0.0317 198.7326); + --secondary: oklch(0.3163 0.019 63.6992); --secondary-foreground: oklch(0.9247 0.0524 66.1732); - --muted: oklch(0.2520 0 0); + --muted: oklch(0.252 0 0); --muted-foreground: oklch(0.7699 0 0); - --accent: oklch(0.2850 0 0); + --accent: oklch(0.285 0 0); --accent-foreground: oklch(0.9491 0 0); - --destructive: oklch(0.6271 0.1936 33.3390); + --destructive: oklch(0.6271 0.1936 33.339); --border: oklch(0.2351 0.0115 91.7467); --input: oklch(0.4017 0 0); --ring: oklch(0.9247 0.0524 66.1732); --chart-1: oklch(0.9247 0.0524 66.1732); - --chart-2: oklch(0.3163 0.0190 63.6992); - --chart-3: oklch(0.2850 0 0); + --chart-2: oklch(0.3163 0.019 63.6992); + --chart-3: oklch(0.285 0 0); --chart-4: oklch(0.3481 0.0219 67.0001); --chart-5: oklch(0.9245 0.0533 67.0855); --sidebar: oklch(0.2103 0.0059 285.8852); --sidebar-foreground: oklch(0.9674 0.0013 286.3752); --sidebar-primary: oklch(0.4882 0.2172 264.3763); - --sidebar-primary-foreground: oklch(1.0000 0 0); + --sidebar-primary-foreground: oklch(1 0 0); --sidebar-accent: oklch(0.2739 0.0055 286.0326); --sidebar-accent-foreground: oklch(0.9674 0.0013 286.3752); --sidebar-border: oklch(0.2739 0.0055 286.0326); - --sidebar-ring: oklch(0.8711 0.0055 286.2860); - --destructive-foreground: oklch(1.0000 0 0); + --sidebar-ring: oklch(0.8711 0.0055 286.286); + --destructive-foreground: oklch(1 0 0); --radius: 0.5rem; - --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-sans: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; --shadow-color: oklch(0 0 0); --shadow-opacity: 0.09; --shadow-blur: 5.5px; @@ -174,20 +193,30 @@ --spacing: 0.2rem; --shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); --shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); - --shadow-sm: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); - --shadow: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); - --shadow-md: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09); - --shadow-lg: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09); - --shadow-xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09); + --shadow-sm: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); + --shadow: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); + --shadow-md: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09); + --shadow-lg: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09); + --shadow-xl: + 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), + 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09); --shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22); } @layer base { - * { - @apply border-border outline-ring/50; + * { + @apply border-border outline-ring/50; } - body { - @apply bg-background text-foreground; - letter-spacing: var(--tracking-normal); + body { + @apply bg-background text-foreground; + letter-spacing: var(--tracking-normal); } -} \ No newline at end of file +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 0b3f5db..97044bd 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -5,36 +5,36 @@ import { AuthProvider } from "@/providers/auth-provider"; import "./globals.css"; const ubuntuSans = Ubuntu_Sans({ - variable: "--font-ubuntu-sans", - subsets: ["latin"], + variable: "--font-ubuntu-sans", + subsets: ["latin"], }); const ubuntuMono = Ubuntu_Mono({ - variable: "--font-geist-mono", - weight: ["400", "700"], - subsets: ["latin"], + variable: "--font-geist-mono", + weight: ["400", "700"], + subsets: ["latin"], }); export const metadata: Metadata = { - title: "MemeGoat", - icons: "/memegoat-color.svg", + title: "MemeGoat", + icons: "/memegoat-color.svg", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - {children} - - - - - ); + return ( + + + + {children} + + + + + ); } diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx index 55caf8f..c18a0fe 100644 --- a/frontend/src/app/not-found.tsx +++ b/frontend/src/app/not-found.tsx @@ -1,34 +1,34 @@ "use client"; +import { AlertCircle, Home } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Home, AlertCircle } from "lucide-react"; export default function NotFound() { - return ( -
-
-
-
- -
-
-

404 - Perdu dans le troupeau ?

-

- On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé. -

-
- -
-
-
- 🐐 -
-
- ); + return ( +
+
+
+
+ +
+
+

+ 404 - Perdu dans le troupeau ? +

+

+ On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé. +

+
+ +
+
+
🐐
+
+ ); } diff --git a/frontend/src/app/robots.ts b/frontend/src/app/robots.ts index 59735bd..311c6b5 100644 --- a/frontend/src/app/robots.ts +++ b/frontend/src/app/robots.ts @@ -1,14 +1,14 @@ import type { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local"; - - return { - rules: { - userAgent: "*", - allow: "/", - disallow: ["/settings/", "/upload/", "/api/"], - }, - sitemap: `${baseUrl}/sitemap.xml`, - }; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local"; + + return { + rules: { + userAgent: "*", + allow: "/", + disallow: ["/settings/", "/upload/", "/api/"], + }, + sitemap: `${baseUrl}/sitemap.xml`, + }; } diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts index 2ddac0e..4b16137 100644 --- a/frontend/src/app/sitemap.ts +++ b/frontend/src/app/sitemap.ts @@ -1,45 +1,47 @@ import type { MetadataRoute } from "next"; -import { ContentService } from "@/services/content.service"; import { CategoryService } from "@/services/category.service"; +import { ContentService } from "@/services/content.service"; export default async function sitemap(): Promise { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local"; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local"; - // Pages statiques - const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map((route) => ({ - url: `${baseUrl}${route}`, - lastModified: new Date(), - changeFrequency: "daily" as const, - priority: route === "" ? 1 : 0.8, - })); + // Pages statiques + const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map( + (route) => ({ + url: `${baseUrl}${route}`, + lastModified: new Date(), + changeFrequency: "daily" as const, + priority: route === "" ? 1 : 0.8, + }), + ); - // Catégories - try { - const categories = await CategoryService.getAll(); - const categoryRoutes = categories.map((category) => ({ - url: `${baseUrl}/category/${category.slug}`, - lastModified: new Date(), - changeFrequency: "weekly" as const, - priority: 0.6, - })); - routes.push(...categoryRoutes); - } catch (error) { - console.error("Sitemap: Failed to fetch categories"); - } + // Catégories + try { + const categories = await CategoryService.getAll(); + const categoryRoutes = categories.map((category) => ({ + url: `${baseUrl}/category/${category.slug}`, + lastModified: new Date(), + changeFrequency: "weekly" as const, + priority: 0.6, + })); + routes.push(...categoryRoutes); + } catch (_error) { + console.error("Sitemap: Failed to fetch categories"); + } - // Mèmes (limité aux 100 derniers pour éviter un sitemap trop gros d'un coup) - try { - const contents = await ContentService.getRecent(100, 0); - const memeRoutes = contents.data.map((meme) => ({ - url: `${baseUrl}/meme/${meme.slug}`, - lastModified: new Date(meme.updatedAt), - changeFrequency: "monthly" as const, - priority: 0.5, - })); - routes.push(...memeRoutes); - } catch (error) { - console.error("Sitemap: Failed to fetch memes"); - } + // Mèmes (limité aux 100 derniers pour éviter un sitemap trop gros d'un coup) + try { + const contents = await ContentService.getRecent(100, 0); + const memeRoutes = contents.data.map((meme) => ({ + url: `${baseUrl}/meme/${meme.slug}`, + lastModified: new Date(meme.updatedAt), + changeFrequency: "monthly" as const, + priority: 0.5, + })); + routes.push(...memeRoutes); + } catch (_error) { + console.error("Sitemap: Failed to fetch memes"); + } - return routes; + return routes; } diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 34dec5d..13e7536 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -1,243 +1,243 @@ "use client"; -import * as React from "react"; +import { + ChevronRight, + Clock, + HelpCircle, + Home, + LayoutGrid, + LogIn, + LogOut, + PlusCircle, + Settings, + TrendingUp, + User as UserIcon, +} from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import * as React from "react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { - Home, - TrendingUp, - Clock, - LayoutGrid, - PlusCircle, - Settings, - HelpCircle, - ChevronRight, - LogOut, - User as UserIcon, - LogIn, -} from "lucide-react"; - -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from "@/components/ui/sidebar"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, } from "@/components/ui/collapsible"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar"; +import { useAuth } from "@/providers/auth-provider"; import { CategoryService } from "@/services/category.service"; import type { Category } from "@/types/content"; -import { useAuth } from "@/providers/auth-provider"; const mainNav = [ - { - title: "Accueil", - url: "/", - icon: Home, - }, - { - title: "Tendances", - url: "/trends", - icon: TrendingUp, - }, - { - title: "Nouveautés", - url: "/recent", - icon: Clock, - }, + { + title: "Accueil", + url: "/", + icon: Home, + }, + { + title: "Tendances", + url: "/trends", + icon: TrendingUp, + }, + { + title: "Nouveautés", + url: "/recent", + icon: Clock, + }, ]; export function AppSidebar() { - const pathname = usePathname(); - const { user, logout, isAuthenticated, isLoading } = useAuth(); - const [categories, setCategories] = React.useState([]); + const pathname = usePathname(); + const { user, logout, isAuthenticated } = useAuth(); + const [categories, setCategories] = React.useState([]); - React.useEffect(() => { - CategoryService.getAll().then(setCategories).catch(console.error); - }, []); + React.useEffect(() => { + CategoryService.getAll().then(setCategories).catch(console.error); + }, []); - return ( - - - -
- 🐐 -
- MemeGoat - -
- - - - {mainNav.map((item) => ( - - - - - {item.title} - - - - ))} - - + return ( + + + +
🐐
+ MemeGoat + +
+ + + + {mainNav.map((item) => ( + + + + + {item.title} + + + + ))} + + - - Explorer - - - - - - - Catégories - - - - - - {categories.map((category) => ( - - - - {category.name} - - - - ))} - - - - - - + + Explorer + + + + + + + Catégories + + + + + + {categories.map((category) => ( + + + + {category.name} + + + + ))} + + + + + + - - Communauté - - - - - - Publier - - - - - - - - - {isAuthenticated && user ? ( - - - - - - - - {user.username.slice(0, 2).toUpperCase()} - - -
- - {user.displayName || user.username} - - {user.email} -
- -
-
- - -
- - - - {user.username.slice(0, 2).toUpperCase()} - - -
- - {user.displayName || user.username} - - {user.email} -
-
-
- - - - - Profil - - - - - - Paramètres - - - - logout()}> - - Déconnexion - -
-
-
- ) : ( - - - - - Se connecter - - - - )} - - - - - Aide - - - -
-
-
- ); + + Communauté + + + + + + Publier + + + + + +
+ + + {isAuthenticated && user ? ( + + + + + + + + {user.username.slice(0, 2).toUpperCase()} + + +
+ + {user.displayName || user.username} + + {user.email} +
+ +
+
+ + +
+ + + + {user.username.slice(0, 2).toUpperCase()} + + +
+ + {user.displayName || user.username} + + {user.email} +
+
+
+ + + + + Profil + + + + + + Paramètres + + + + logout()}> + + Déconnexion + +
+
+
+ ) : ( + + + + + Se connecter + + + + )} + + + + + Aide + + + +
+
+
+ ); } diff --git a/frontend/src/components/category-content.tsx b/frontend/src/components/category-content.tsx index 9c05566..80e211e 100644 --- a/frontend/src/components/category-content.tsx +++ b/frontend/src/components/category-content.tsx @@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list"; import { ContentService } from "@/services/content.service"; export function CategoryContent({ slug }: { slug: string }) { - const fetchFn = React.useCallback((p: { limit: number; offset: number }) => - ContentService.getExplore({ ...p, category: slug }), - [slug]); + const fetchFn = React.useCallback( + (p: { limit: number; offset: number }) => + ContentService.getExplore({ ...p, category: slug }), + [slug], + ); - return ; + return ; } diff --git a/frontend/src/components/content-card.tsx b/frontend/src/components/content-card.tsx index 14a74eb..ad6855f 100644 --- a/frontend/src/components/content-card.tsx +++ b/frontend/src/components/content-card.tsx @@ -1,131 +1,143 @@ "use client"; -import * as React from "react"; +import { Eye, Heart, MoreHorizontal, Share2 } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import { Heart, MessageSquare, Share2, MoreHorizontal, Eye } from "lucide-react"; -import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; +import { useRouter } from "next/navigation"; +import * as React from "react"; +import { toast } from "sonner"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; -import type { Content } from "@/types/content"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/components/ui/card"; import { useAuth } from "@/providers/auth-provider"; import { FavoriteService } from "@/services/favorite.service"; -import { useRouter } from "next/navigation"; -import { toast } from "sonner"; +import type { Content } from "@/types/content"; interface ContentCardProps { - content: Content; + content: Content; } export function ContentCard({ content }: ContentCardProps) { - const { isAuthenticated, user } = useAuth(); - const router = useRouter(); - const [isLiked, setIsLiked] = React.useState(false); - const [likesCount, setLikesCount] = React.useState(content.favoritesCount); + const { isAuthenticated } = useAuth(); + const router = useRouter(); + const [isLiked, setIsLiked] = React.useState(false); + const [likesCount, setLikesCount] = React.useState(content.favoritesCount); - const handleLike = async (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); + const handleLike = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - if (!isAuthenticated) { - toast.error("Vous devez être connecté pour liker un mème"); - router.push("/login"); - return; - } + if (!isAuthenticated) { + toast.error("Vous devez être connecté pour liker un mème"); + router.push("/login"); + return; + } - try { - if (isLiked) { - await FavoriteService.remove(content.id); - setIsLiked(false); - setLikesCount(prev => prev - 1); - } else { - await FavoriteService.add(content.id); - setIsLiked(true); - setLikesCount(prev => prev + 1); - } - } catch (error) { - toast.error("Une erreur est survenue"); - } - }; + try { + if (isLiked) { + await FavoriteService.remove(content.id); + setIsLiked(false); + setLikesCount((prev) => prev - 1); + } else { + await FavoriteService.add(content.id); + setIsLiked(true); + setLikesCount((prev) => prev + 1); + } + } catch (_error) { + toast.error("Une erreur est survenue"); + } + }; - return ( - - - - - {content.author.username[0].toUpperCase()} - -
- - {content.author.displayName || content.author.username} - - - {new Date(content.createdAt).toLocaleDateString('fr-FR')} - -
- -
- - - {content.type === "image" ? ( - {content.title} - ) : ( - - -
-
- - - -
- -
- -
-

{content.title}

-
- {content.tags.slice(0, 3).map((tag, i) => ( - - #{typeof tag === 'string' ? tag : tag.name} - - ))} -
-
-
-
- ); + return ( + + + + + {content.author.username[0].toUpperCase()} + +
+ + {content.author.displayName || content.author.username} + + + {new Date(content.createdAt).toLocaleDateString("fr-FR")} + +
+ +
+ + + {content.type === "image" ? ( + {content.title} + ) : ( + + +
+
+ + + +
+ +
+ +
+

{content.title}

+
+ {content.tags.slice(0, 3).map((tag, _i) => ( + + #{typeof tag === "string" ? tag : tag.name} + + ))} +
+
+
+
+ ); } diff --git a/frontend/src/components/content-list.tsx b/frontend/src/components/content-list.tsx index 7021215..235e057 100644 --- a/frontend/src/components/content-list.tsx +++ b/frontend/src/components/content-list.tsx @@ -1,89 +1,93 @@ "use client"; import * as React from "react"; -import { ContentCard } from "@/components/content-card"; -import { ContentService } from "@/services/content.service"; -import type { Content, PaginatedResponse } from "@/types/content"; -import { Spinner } from "@/components/ui/spinner"; - import { useInfiniteScroll } from "@/app/(dashboard)/_hooks/use-infinite-scroll"; +import { ContentCard } from "@/components/content-card"; +import { Spinner } from "@/components/ui/spinner"; +import type { Content, PaginatedResponse } from "@/types/content"; interface ContentListProps { - fetchFn: (params: { limit: number; offset: number }) => Promise>; - title?: string; + fetchFn: (params: { + limit: number; + offset: number; + }) => Promise>; + title?: string; } export function ContentList({ fetchFn, title }: ContentListProps) { - const [contents, setContents] = React.useState([]); - const [loading, setLoading] = React.useState(true); - const [offset, setOffset] = React.useState(0); - const [hasMore, setHasMore] = React.useState(true); + const [contents, setContents] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const [offset, setOffset] = React.useState(0); + const [hasMore, setHasMore] = React.useState(true); - const loadMore = React.useCallback(async () => { - if (!hasMore || loading) return; - - setLoading(true); - try { - const response = await fetchFn({ - limit: 10, - offset: offset + 10, - }); - - setContents(prev => [...prev, ...response.data]); - setOffset(prev => prev + 10); - setHasMore(response.data.length === 10); - } catch (error) { - console.error("Failed to load more contents:", error); - } finally { - setLoading(false); - } - }, [offset, hasMore, loading, fetchFn]); + const loadMore = React.useCallback(async () => { + if (!hasMore || loading) return; - const { loaderRef } = useInfiniteScroll({ - hasMore, - loading, - onLoadMore: loadMore, - }); + setLoading(true); + try { + const response = await fetchFn({ + limit: 10, + offset: offset + 10, + }); - React.useEffect(() => { - const fetchInitial = async () => { - setLoading(true); - try { - const response = await fetchFn({ - limit: 10, - offset: 0, - }); - setContents(response.data); - setHasMore(response.data.length === 10); - } catch (error) { - console.error("Failed to fetch contents:", error); - } finally { - setLoading(false); - } - }; + setContents((prev) => [...prev, ...response.data]); + setOffset((prev) => prev + 10); + setHasMore(response.data.length === 10); + } catch (error) { + console.error("Failed to load more contents:", error); + } finally { + setLoading(false); + } + }, [offset, hasMore, loading, fetchFn]); - fetchInitial(); - }, [fetchFn]); + const { loaderRef } = useInfiniteScroll({ + hasMore, + loading, + onLoadMore: loadMore, + }); + React.useEffect(() => { + const fetchInitial = async () => { + setLoading(true); + try { + const response = await fetchFn({ + limit: 10, + offset: 0, + }); + setContents(response.data); + setHasMore(response.data.length === 10); + } catch (error) { + console.error("Failed to fetch contents:", error); + } finally { + setLoading(false); + } + }; - return ( -
- {title &&

{title}

} -
- {contents.map((content) => ( - - ))} -
- -
- {loading && } - {!hasMore && contents.length > 0 && ( -

Vous avez atteint la fin ! 🐐

- )} - {!loading && contents.length === 0 && ( -

Aucun mème trouvé ici... pour l'instant !

- )} -
-
- ); + fetchInitial(); + }, [fetchFn]); + + return ( +
+ {title &&

{title}

} +
+ {contents.map((content) => ( + + ))} +
+ +
+ {loading && } + {!hasMore && contents.length > 0 && ( +

+ Vous avez atteint la fin ! 🐐 +

+ )} + {!loading && contents.length === 0 && ( +

+ Aucun mème trouvé ici... pour l'instant ! +

+ )} +
+
+ ); } diff --git a/frontend/src/components/content-skeleton.tsx b/frontend/src/components/content-skeleton.tsx index f0605d3..f274b1a 100644 --- a/frontend/src/components/content-skeleton.tsx +++ b/frontend/src/components/content-skeleton.tsx @@ -1,35 +1,40 @@ -import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; export function ContentSkeleton() { - return ( - - - -
- - -
-
- - - - -
-
- - -
- -
-
- -
- - -
-
-
-
- ); + return ( + + + +
+ + +
+
+ + + + +
+
+ + +
+ +
+
+ +
+ + +
+
+
+
+ ); } diff --git a/frontend/src/components/home-content.tsx b/frontend/src/components/home-content.tsx index a785021..e297093 100644 --- a/frontend/src/components/home-content.tsx +++ b/frontend/src/components/home-content.tsx @@ -1,35 +1,37 @@ "use client"; +import { useSearchParams } from "next/navigation"; import * as React from "react"; import { ContentList } from "@/components/content-list"; import { ContentService } from "@/services/content.service"; -import { useSearchParams } from "next/navigation"; export function HomeContent() { - const searchParams = useSearchParams(); - - const sort = (searchParams.get("sort") as "trend" | "recent") || "trend"; - const category = searchParams.get("category") || undefined; - const tag = searchParams.get("tag") || undefined; - const query = searchParams.get("query") || undefined; + const searchParams = useSearchParams(); - const fetchFn = React.useCallback((params: { limit: number; offset: number }) => - ContentService.getExplore({ - ...params, - sort, - category, - tag, - query - }), - [sort, category, tag, query]); + const sort = (searchParams.get("sort") as "trend" | "recent") || "trend"; + const category = searchParams.get("category") || undefined; + const tag = searchParams.get("tag") || undefined; + const query = searchParams.get("query") || undefined; - const title = query - ? `Résultats pour "${query}"` - : category - ? `Catégorie : ${category}` - : sort === "trend" - ? "Tendances du moment" - : "Nouveautés"; + const fetchFn = React.useCallback( + (params: { limit: number; offset: number }) => + ContentService.getExplore({ + ...params, + sort, + category, + tag, + query, + }), + [sort, category, tag, query], + ); - return ; + const title = query + ? `Résultats pour "${query}"` + : category + ? `Catégorie : ${category}` + : sort === "trend" + ? "Tendances du moment" + : "Nouveautés"; + + return ; } diff --git a/frontend/src/components/mobile-filters.tsx b/frontend/src/components/mobile-filters.tsx index cc60f5f..0550bf3 100644 --- a/frontend/src/components/mobile-filters.tsx +++ b/frontend/src/components/mobile-filters.tsx @@ -1,146 +1,153 @@ "use client"; -import * as React from "react"; import { Filter, Search } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import { Input } from "@/components/ui/input"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import * as React from "react"; import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useRouter, useSearchParams, usePathname } from "next/navigation"; +import { Separator } from "@/components/ui/separator"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { CategoryService } from "@/services/category.service"; import type { Category } from "@/types/content"; export function MobileFilters() { - const router = useRouter(); - const searchParams = useSearchParams(); - const pathname = usePathname(); - - const [categories, setCategories] = React.useState([]); - const [query, setQuery] = React.useState(searchParams.get("query") || ""); - const [open, setOpen] = React.useState(false); + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); - React.useEffect(() => { - if (open) { - CategoryService.getAll().then(setCategories).catch(console.error); - } - }, [open]); + const [categories, setCategories] = React.useState([]); + const [query, setQuery] = React.useState(searchParams.get("query") || ""); + const [open, setOpen] = React.useState(false); - const updateSearch = React.useCallback((name: string, value: string | null) => { - const params = new URLSearchParams(searchParams.toString()); - if (value) { - params.set(name, value); - } else { - params.delete(name); - } - router.push(`${pathname}?${params.toString()}`); - }, [router, pathname, searchParams]); + React.useEffect(() => { + if (open) { + CategoryService.getAll().then(setCategories).catch(console.error); + } + }, [open]); - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - updateSearch("query", query); - setOpen(false); - }; + const updateSearch = React.useCallback( + (name: string, value: string | null) => { + const params = new URLSearchParams(searchParams.toString()); + if (value) { + params.set(name, value); + } else { + params.delete(name); + } + router.push(`${pathname}?${params.toString()}`); + }, + [router, pathname, searchParams], + ); - const currentSort = searchParams.get("sort") || "trend"; - const currentCategory = searchParams.get("category"); + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + updateSearch("query", query); + setOpen(false); + }; - return ( -
- - - - - - - Recherche & Filtres - -
-
- - setQuery(e.target.value)} - /> - - - -
-
-

Trier par

-
- updateSearch("sort", "trend")} - > - Tendances - - updateSearch("sort", "recent")} - > - Récent - -
-
- -
-

Catégories

-
- updateSearch("category", null)} - > - Tout - - {categories.map((cat) => ( - updateSearch("category", cat.slug)} - > - {cat.name} - - ))} -
-
- -
-

Tags populaires

-
- {["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(tag => ( - updateSearch("tag", searchParams.get("tag") === tag ? null : tag)} - > - #{tag} - - ))} -
-
-
-
-
-
-
-
- ); + const currentSort = searchParams.get("sort") || "trend"; + const currentCategory = searchParams.get("category"); + + return ( +
+ + + + + + + Recherche & Filtres + +
+
+ + setQuery(e.target.value)} + /> + + + +
+
+

Trier par

+
+ updateSearch("sort", "trend")} + > + Tendances + + updateSearch("sort", "recent")} + > + Récent + +
+
+ +
+

Catégories

+
+ updateSearch("category", null)} + > + Tout + + {categories.map((cat) => ( + updateSearch("category", cat.slug)} + > + {cat.name} + + ))} +
+
+ +
+

Tags populaires

+
+ {["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map( + (tag) => ( + + updateSearch("tag", searchParams.get("tag") === tag ? null : tag) + } + > + #{tag} + + ), + )} +
+
+
+
+
+
+
+
+ ); } diff --git a/frontend/src/components/search-sidebar.tsx b/frontend/src/components/search-sidebar.tsx index 1794281..5acfc49 100644 --- a/frontend/src/components/search-sidebar.tsx +++ b/frontend/src/components/search-sidebar.tsx @@ -1,132 +1,139 @@ "use client"; +import { Filter, Search } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import * as React from "react"; -import { Search, Filter } from "lucide-react"; -import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { Separator } from "@/components/ui/separator"; +import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useRouter, useSearchParams, usePathname } from "next/navigation"; +import { Separator } from "@/components/ui/separator"; import { CategoryService } from "@/services/category.service"; import type { Category } from "@/types/content"; export function SearchSidebar() { - const router = useRouter(); - const searchParams = useSearchParams(); - const pathname = usePathname(); - - const [categories, setCategories] = React.useState([]); - const [query, setQuery] = React.useState(searchParams.get("query") || ""); + const router = useRouter(); + const searchParams = useSearchParams(); + const pathname = usePathname(); - React.useEffect(() => { - CategoryService.getAll().then(setCategories).catch(console.error); - }, []); + const [categories, setCategories] = React.useState([]); + const [query, setQuery] = React.useState(searchParams.get("query") || ""); - const updateSearch = React.useCallback((name: string, value: string | null) => { - const params = new URLSearchParams(searchParams.toString()); - if (value) { - params.set(name, value); - } else { - params.delete(name); - } - // If we are not on explore/trends/recent, maybe we should redirect to home? - // For now, let's just update the URL. - router.push(`${pathname}?${params.toString()}`); - }, [router, pathname, searchParams]); + React.useEffect(() => { + CategoryService.getAll().then(setCategories).catch(console.error); + }, []); - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - updateSearch("query", query); - }; + const updateSearch = React.useCallback( + (name: string, value: string | null) => { + const params = new URLSearchParams(searchParams.toString()); + if (value) { + params.set(name, value); + } else { + params.delete(name); + } + // If we are not on explore/trends/recent, maybe we should redirect to home? + // For now, let's just update the URL. + router.push(`${pathname}?${params.toString()}`); + }, + [router, pathname, searchParams], + ); - const currentSort = searchParams.get("sort") || "trend"; - const currentCategory = searchParams.get("category"); + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + updateSearch("query", query); + }; - return ( - - ); + return ( + + ); } diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx index 4a8cca4..7fa8fa3 100644 --- a/frontend/src/components/ui/accordion.tsx +++ b/frontend/src/components/ui/accordion.tsx @@ -1,66 +1,66 @@ -"use client" +"use client"; -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDownIcon } from "lucide-react" +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Accordion({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function AccordionItem({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AccordionTrigger({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) + return ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + + ); } function AccordionContent({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - -
{children}
-
- ) + return ( + +
{children}
+
+ ); } -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index 0863e40..2b02714 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -1,157 +1,156 @@ -"use client" +"use client"; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; +import type * as React from "react"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; function AlertDialog({ - ...props + ...props }: React.ComponentProps) { - return + return ; } function AlertDialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogPortal({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogContent({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - - ) + return ( + + + + + ); } function AlertDialogHeader({ - className, - ...props + className, + ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function AlertDialogFooter({ - className, - ...props + className, + ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function AlertDialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogAction({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogCancel({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx index 1421354..ecc8029 100644 --- a/frontend/src/components/ui/alert.tsx +++ b/frontend/src/components/ui/alert.tsx @@ -1,66 +1,66 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", - { - variants: { - variant: { - default: "bg-card text-card-foreground", - destructive: - "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); function Alert({ - className, - variant, - ...props + className, + variant, + ...props }: React.ComponentProps<"div"> & VariantProps) { - return ( -
- ) + return ( +
+ ); } function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function AlertDescription({ - className, - ...props + className, + ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/frontend/src/components/ui/aspect-ratio.tsx b/frontend/src/components/ui/aspect-ratio.tsx index 3df3fd0..51fa2c6 100644 --- a/frontend/src/components/ui/aspect-ratio.tsx +++ b/frontend/src/components/ui/aspect-ratio.tsx @@ -1,11 +1,11 @@ -"use client" +"use client"; -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; function AspectRatio({ - ...props + ...props }: React.ComponentProps) { - return + return ; } -export { AspectRatio } +export { AspectRatio }; diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx index 71e428b..a03b6cf 100644 --- a/frontend/src/components/ui/avatar.tsx +++ b/frontend/src/components/ui/avatar.tsx @@ -1,53 +1,53 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Avatar({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AvatarImage({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AvatarFallback({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index fd3a406..7fa2c18 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -1,46 +1,46 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); function Badge({ - className, - variant, - asChild = false, - ...props + className, + variant, + asChild = false, + ...props }: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; - return ( - - ) + return ( + + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx index eb88f32..acf0a22 100644 --- a/frontend/src/components/ui/breadcrumb.tsx +++ b/frontend/src/components/ui/breadcrumb.tsx @@ -1,109 +1,109 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import type * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return