Marketplace inversée (Next.js 16 App Router + Supabase Cloud) : auth, schéma + RLS, publication de besoins avec upload d'images, feed avec filtres, offres, acceptation via RPC, messagerie temps réel, dashboards, et Dockerfile pour déploiement Coolify.
57 lines
1.4 KiB
TypeScript
57 lines
1.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Image from "next/image";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
export function ImageUploader({
|
|
name,
|
|
maxFiles = 5,
|
|
}: {
|
|
name: string;
|
|
maxFiles?: number;
|
|
}) {
|
|
const [previews, setPreviews] = useState<string[]>([]);
|
|
|
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const files = Array.from(e.target.files ?? []).slice(0, maxFiles);
|
|
setPreviews((old) => {
|
|
old.forEach((url) => URL.revokeObjectURL(url));
|
|
return files.map((file) => URL.createObjectURL(file));
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<Label htmlFor={name}>Photos (optionnel, {maxFiles} max)</Label>
|
|
<Input
|
|
id={name}
|
|
name={name}
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
onChange={handleChange}
|
|
/>
|
|
{previews.length > 0 && (
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{previews.map((src, i) => (
|
|
<div
|
|
key={src}
|
|
className="relative h-20 w-20 overflow-hidden rounded-md border"
|
|
>
|
|
<Image
|
|
src={src}
|
|
alt={`Aperçu ${i + 1}`}
|
|
fill
|
|
className="object-cover"
|
|
unoptimized
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|