Apply consistent import ordering and indentation in frontend and backend files. Ensure better maintainability and adherence to code style standards.
328 lines
9.2 KiB
TypeScript
328 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { Image as ImageIcon, Loader2, LogIn, Upload, X } from "lucide-react";
|
|
import NextImage from "next/image";
|
|
import Link from "next/link";
|
|
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,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import { useAuth } from "@/providers/auth-provider";
|
|
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(),
|
|
});
|
|
|
|
type UploadFormValues = z.infer<typeof uploadSchema>;
|
|
|
|
export default function UploadPage() {
|
|
const router = useRouter();
|
|
const { isAuthenticated, isLoading } = useAuth();
|
|
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: "",
|
|
},
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (isAuthenticated) {
|
|
CategoryService.getAll().then(setCategories).catch(console.error);
|
|
}
|
|
}, [isAuthenticated]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-[400px] items-center justify-center">
|
|
<Spinner className="h-8 w-8 text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<div className="max-w-2xl mx-auto py-8 px-4">
|
|
<Card className="text-center p-12">
|
|
<CardHeader>
|
|
<div className="mx-auto bg-primary/10 p-4 rounded-full w-fit mb-4">
|
|
<LogIn className="h-8 w-8 text-primary" />
|
|
</div>
|
|
<CardTitle>Connexion requise</CardTitle>
|
|
<CardDescription>
|
|
Vous devez être connecté pour partager vos meilleurs mèmes avec la
|
|
communauté.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button asChild className="w-full sm:w-auto">
|
|
<Link href="/login">Se connecter</Link>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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: 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 ? (
|
|
<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>
|
|
)}
|
|
/>
|
|
|
|
<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="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>
|
|
);
|
|
}
|