refactor: apply consistent formatting to improve code quality
Ensure uniform code formatting across components by aligning with the established code style. Adjust imports, indentation, and spacing to enhance readability and maintainability.
This commit is contained in:
@@ -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<typeof loginSchema>;
|
||||
|
||||
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<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
const form = useForm<LoginFormValues>({
|
||||
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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Connexion</CardTitle>
|
||||
<CardDescription>
|
||||
Entrez vos identifiants pour accéder à votre compte MemeGoat.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="goat@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Connexion en cours..." : "Se connecter"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-2">
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
Vous n'avez pas de compte ?{" "}
|
||||
<Link href="/register" className="text-primary hover:underline font-medium">
|
||||
S'inscrire
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Connexion</CardTitle>
|
||||
<CardDescription>
|
||||
Entrez vos identifiants pour accéder à votre compte MemeGoat.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="goat@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Connexion en cours..." : "Se connecter"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-2">
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
Vous n'avez pas de compte ?{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
S'inscrire
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<typeof registerSchema>;
|
||||
|
||||
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<RegisterFormValues>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
displayName: "",
|
||||
},
|
||||
});
|
||||
const form = useForm<RegisterFormValues>({
|
||||
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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Inscription</CardTitle>
|
||||
<CardDescription>
|
||||
Rejoignez la communauté MemeGoat dès aujourd'hui.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Pseudo</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="supergoat" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="goat@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="displayName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nom d'affichage (Optionnel)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Le Roi des Chèvres" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Création du compte..." : "S'inscrire"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-2">
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
Vous avez déjà un compte ?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline font-medium">
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-zinc-50 dark:bg-zinc-950 p-4">
|
||||
<div className="w-full max-w-md space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Inscription</CardTitle>
|
||||
<CardDescription>
|
||||
Rejoignez la communauté MemeGoat dès aujourd'hui.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Pseudo</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="supergoat" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="goat@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="displayName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Nom d'affichage (Optionnel)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Le Roi des Chèvres" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? "Création du compte..." : "S'inscrire"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col space-y-2">
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
Vous avez déjà un compte ?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline font-medium">
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Content | null>(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<Content | null>(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 (
|
||||
<Dialog open onOpenChange={(open) => !open && router.back()}>
|
||||
<DialogContent className="max-w-3xl p-0 overflow-hidden bg-transparent border-none">
|
||||
<DialogTitle className="sr-only">{content?.title || "Détail du mème"}</DialogTitle>
|
||||
<DialogDescription className="sr-only">Affiche le mème en grand avec ses détails</DialogDescription>
|
||||
{loading ? (
|
||||
<div className="h-[500px] flex items-center justify-center bg-zinc-950/50 rounded-lg">
|
||||
<Spinner className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
) : content ? (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg overflow-hidden">
|
||||
<ContentCard content={content} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 bg-white dark:bg-zinc-900 rounded-lg text-center">
|
||||
<p>Impossible de charger ce mème.</p>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog open onOpenChange={(open) => !open && router.back()}>
|
||||
<DialogContent className="max-w-3xl p-0 overflow-hidden bg-transparent border-none">
|
||||
<DialogTitle className="sr-only">
|
||||
{content?.title || "Détail du mème"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Affiche le mème en grand avec ses détails
|
||||
</DialogDescription>
|
||||
{loading ? (
|
||||
<div className="h-[500px] flex items-center justify-center bg-zinc-950/50 rounded-lg">
|
||||
<Spinner className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
) : content ? (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-lg overflow-hidden">
|
||||
<ContentCard content={content} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 bg-white dark:bg-zinc-900 rounded-lg text-center">
|
||||
<p>Impossible de charger ce mème.</p>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function Default() {
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const loaderRef = React.useRef<HTMLDivElement>(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 };
|
||||
}
|
||||
|
||||
@@ -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<Metadata> {
|
||||
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 <CategoryContent slug={slug} />;
|
||||
const { slug } = await params;
|
||||
return <CategoryContent slug={slug} />;
|
||||
}
|
||||
|
||||
@@ -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<Category[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||
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 (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<LayoutGrid className="h-6 w-6" />
|
||||
<h1 className="text-3xl font-bold">Catégories</h1>
|
||||
</div>
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<LayoutGrid className="h-6 w-6" />
|
||||
<h1 className="text-3xl font-bold">Catégories</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{loading ? (
|
||||
Array.from({ length: 6 }).map((_, i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardHeader className="h-24 bg-zinc-100 dark:bg-zinc-800 rounded-t-lg" />
|
||||
<CardContent className="h-12" />
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
categories.map((category) => (
|
||||
<Link key={category.id} href={`/category/${category.slug}`}>
|
||||
<Card className="hover:border-primary transition-colors cursor-pointer group h-full">
|
||||
<CardHeader className="bg-zinc-50 dark:bg-zinc-900 group-hover:bg-primary/5 transition-colors">
|
||||
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{category.description || `Découvrez tous les mèmes de la catégorie ${category.name}.`}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{loading
|
||||
? Array.from({ length: 6 }).map((_, i) => (
|
||||
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
|
||||
<Card key={`skeleton-${i}`} className="animate-pulse">
|
||||
<CardHeader className="h-24 bg-zinc-100 dark:bg-zinc-800 rounded-t-lg" />
|
||||
<CardContent className="h-12" />
|
||||
</Card>
|
||||
))
|
||||
: categories.map((category) => (
|
||||
<Link key={category.id} href={`/category/${category.slug}`}>
|
||||
<Card className="hover:border-primary transition-colors cursor-pointer group h-full">
|
||||
<CardHeader className="bg-zinc-50 dark:bg-zinc-900 group-hover:bg-primary/5 transition-colors">
|
||||
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{category.description ||
|
||||
`Découvrez tous les mèmes de la catégorie ${category.name}.`}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="flex flex-row overflow-hidden">
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 lg:hidden">
|
||||
<SidebarTrigger />
|
||||
<div className="flex-1" />
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
|
||||
{children}
|
||||
{modal}
|
||||
</main>
|
||||
<React.Suspense fallback={null}>
|
||||
<MobileFilters />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchSidebar />
|
||||
</React.Suspense>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset className="flex flex-row overflow-hidden">
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 lg:hidden">
|
||||
<SidebarTrigger />
|
||||
<div className="flex-1" />
|
||||
</header>
|
||||
<main className="flex-1 overflow-y-auto bg-zinc-50 dark:bg-zinc-950">
|
||||
{children}
|
||||
{modal}
|
||||
</main>
|
||||
<React.Suspense fallback={null}>
|
||||
<MobileFilters />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchSidebar />
|
||||
</React.Suspense>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ContentSkeleton } from "@/components/content-skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<ContentSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
/* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
|
||||
<ContentSkeleton key={`loading-skeleton-${i}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Metadata> {
|
||||
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 (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<Link href="/" className="inline-flex items-center text-sm mb-6 hover:text-primary transition-colors">
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
Retour au flux
|
||||
</Link>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2">
|
||||
<ContentCard content={content} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border">
|
||||
<h2 className="font-bold text-lg mb-4">À propos de ce mème</h2>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Publié par</p>
|
||||
<p className="font-medium">{content.author.displayName || content.author.username}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Date</p>
|
||||
<p className="font-medium">{new Date(content.createdAt).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}</p>
|
||||
</div>
|
||||
{content.description && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Description</p>
|
||||
<p>{content.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border text-center">
|
||||
<p className="text-sm text-muted-foreground mb-4">Envie de créer votre propre mème ?</p>
|
||||
<Button className="w-full">Utiliser ce template</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
notFound();
|
||||
}
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const content = await ContentService.getOne(slug);
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-sm mb-6 hover:text-primary transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
Retour au flux
|
||||
</Link>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2">
|
||||
<ContentCard content={content} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border">
|
||||
<h2 className="font-bold text-lg mb-4">À propos de ce mème</h2>
|
||||
<div className="space-y-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Publié par</p>
|
||||
<p className="font-medium">
|
||||
{content.author.displayName || content.author.username}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Date</p>
|
||||
<p className="font-medium">
|
||||
{new Date(content.createdAt).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{content.description && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Description</p>
|
||||
<p>{content.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border text-center">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Envie de créer votre propre mème ?
|
||||
</p>
|
||||
<Button className="w-full">Utiliser ce template</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (_error) {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<React.Suspense fallback={
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<Spinner className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
}>
|
||||
<HomeContent />
|
||||
</React.Suspense>
|
||||
);
|
||||
return (
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<Spinner className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HomeContent />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border shadow-sm mb-8">
|
||||
<div className="flex flex-col md:flex-row items-center gap-8">
|
||||
<Avatar className="h-32 w-32 border-4 border-primary/10">
|
||||
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
||||
<AvatarFallback className="text-4xl">
|
||||
{user.username.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 text-center md:text-left space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{user.displayName || user.username}</h1>
|
||||
<p className="text-muted-foreground">@{user.username}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Membre depuis {new Date(user.createdAt).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/settings">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Paramètres
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => logout()} className="text-red-500 hover:text-red-600 hover:bg-red-50">
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Déconnexion
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border shadow-sm mb-8">
|
||||
<div className="flex flex-col md:flex-row items-center gap-8">
|
||||
<Avatar className="h-32 w-32 border-4 border-primary/10">
|
||||
<AvatarImage src={user.avatarUrl} alt={user.username} />
|
||||
<AvatarFallback className="text-4xl">
|
||||
{user.username.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 text-center md:text-left space-y-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">
|
||||
{user.displayName || user.username}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">@{user.username}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Membre depuis{" "}
|
||||
{new Date(user.createdAt).toLocaleDateString("fr-FR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/settings">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Paramètres
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => logout()}
|
||||
className="text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Déconnexion
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="memes" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-8">
|
||||
<TabsTrigger value="memes">Mes Mèmes</TabsTrigger>
|
||||
<TabsTrigger value="favorites">Mes Favoris</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="memes">
|
||||
<ContentList fetchFn={fetchMyMemes} />
|
||||
</TabsContent>
|
||||
<TabsContent value="favorites">
|
||||
<ContentList fetchFn={fetchMyFavorites} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
<Tabs defaultValue="memes" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-8">
|
||||
<TabsTrigger value="memes">Mes Mèmes</TabsTrigger>
|
||||
<TabsTrigger value="favorites">Mes Favoris</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="memes">
|
||||
<ContentList fetchFn={fetchMyMemes} />
|
||||
</TabsContent>
|
||||
<TabsContent value="favorites">
|
||||
<ContentList fetchFn={fetchMyFavorites} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <ContentList fetchFn={fetchFn} title="Nouveaux Mèmes" />;
|
||||
return <ContentList fetchFn={fetchFn} title="Nouveaux Mèmes" />;
|
||||
}
|
||||
|
||||
@@ -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 <ContentList fetchFn={fetchFn} title="Top Tendances" />;
|
||||
return <ContentList fetchFn={fetchFn} title="Top Tendances" />;
|
||||
}
|
||||
|
||||
@@ -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<typeof uploadSchema>;
|
||||
|
||||
export default function UploadPage() {
|
||||
const router = useRouter();
|
||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||
const [file, setFile] = React.useState<File | null>(null);
|
||||
const [preview, setPreview] = React.useState<string | null>(null);
|
||||
const [isUploading, setIsUploading] = React.useState(false);
|
||||
const router = useRouter();
|
||||
const [categories, setCategories] = React.useState<Category[]>([]);
|
||||
const [file, setFile] = React.useState<File | null>(null);
|
||||
const [preview, setPreview] = React.useState<string | null>(null);
|
||||
const [isUploading, setIsUploading] = React.useState(false);
|
||||
|
||||
const form = useForm<UploadFormValues>({
|
||||
resolver: zodResolver(uploadSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
type: "meme",
|
||||
tags: "",
|
||||
},
|
||||
});
|
||||
const form = useForm<UploadFormValues>({
|
||||
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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-5 w-5" />
|
||||
Partager un mème
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<FormLabel>Fichier (Image ou GIF)</FormLabel>
|
||||
{!preview ? (
|
||||
<div
|
||||
className="border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-900 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
onClick={() => document.getElementById("file-upload")?.click()}
|
||||
>
|
||||
<div className="bg-primary/10 p-4 rounded-full mb-4">
|
||||
<ImageIcon className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">PNG, JPG ou GIF jusqu'à 10Mo</p>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*,.gif"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
||||
<img
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
className="max-h-[400px] mx-auto object-contain"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 rounded-full"
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-8 px-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-5 w-5" />
|
||||
Partager un mème
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<FormLabel>Fichier (Image ou GIF)</FormLabel>
|
||||
{!preview ? (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-900 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
onClick={() => document.getElementById("file-upload")?.click()}
|
||||
>
|
||||
<div className="bg-primary/10 p-4 rounded-full mb-4">
|
||||
<ImageIcon className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="font-medium">Cliquez pour choisir un fichier</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
PNG, JPG ou GIF jusqu'à 10Mo
|
||||
</p>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*,.gif"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
|
||||
<div className="relative h-[400px] w-full">
|
||||
<NextImage
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 rounded-full z-10"
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Titre</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Un titre génial pour votre mème..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Titre</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Un titre génial pour votre mème..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Format</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionnez un format" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="meme">Image fixe</SelectItem>
|
||||
<SelectItem value="gif">GIF Animé</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Format</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionnez un format" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="meme">Image fixe</SelectItem>
|
||||
<SelectItem value="gif">GIF Animé</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="categoryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Catégorie</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionnez une catégorie" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{categories.map(cat => (
|
||||
<SelectItem key={cat.id} value={cat.id}>{cat.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="categoryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Catégorie</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Sélectionnez une catégorie" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{categories.map((cat) => (
|
||||
<SelectItem key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="funny, coding, goat..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Séparez les tags par des virgules.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="funny, coding, goat..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Séparez les tags par des virgules.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isUploading}>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Upload en cours...
|
||||
</>
|
||||
) : (
|
||||
"Publier le mème"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
<Button type="submit" className="w-full" disabled={isUploading}>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Upload en cours...
|
||||
</>
|
||||
) : (
|
||||
"Publier le mème"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-orange-100 dark:bg-orange-900/30 p-4 rounded-full">
|
||||
<AlertTriangle className="h-12 w-12 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">Oups ! Une erreur est survenue</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur pied.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Button onClick={reset} size="lg" className="gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Réessayer
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="gap-2">
|
||||
<Link href="/">
|
||||
<Home className="h-4 w-4" />
|
||||
Retourner à l'accueil
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-orange-100 dark:bg-orange-900/30 p-4 rounded-full">
|
||||
<AlertTriangle className="h-12 w-12 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">
|
||||
Oups ! Une erreur est survenue
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur
|
||||
pied.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Button onClick={reset} size="lg" className="gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Réessayer
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg" className="gap-2">
|
||||
<Link href="/">
|
||||
<Home className="h-4 w-4" />
|
||||
Retourner à l'accueil
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${ubuntuSans.variable} ${ubuntuMono.variable} antialiased`}
|
||||
>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
return (
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${ubuntuSans.variable} ${ubuntuMono.variable} antialiased`}
|
||||
>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-red-100 dark:bg-red-900/30 p-4 rounded-full">
|
||||
<AlertCircle className="h-12 w-12 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">404 - Perdu dans le troupeau ?</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Button asChild size="lg" className="gap-2">
|
||||
<Link href="/">
|
||||
<Home className="h-4 w-4" />
|
||||
Retourner à l'accueil
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 text-8xl grayscale opacity-20 select-none">
|
||||
🐐
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-950 px-4">
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-red-100 dark:bg-red-900/30 p-4 rounded-full">
|
||||
<AlertCircle className="h-12 w-12 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">
|
||||
404 - Perdu dans le troupeau ?
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Button asChild size="lg" className="gap-2">
|
||||
<Link href="/">
|
||||
<Home className="h-4 w-4" />
|
||||
Retourner à l'accueil
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 text-8xl grayscale opacity-20 select-none">🐐</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<MetadataRoute.Sitemap> {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user