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,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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user