126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
import { useCallback, useRef } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Upload, X } from "lucide-react";
|
|
|
|
interface ImageUploadFieldProps {
|
|
label: string;
|
|
currentUrl?: string;
|
|
file: File | null;
|
|
onFileChange: (file: File | null) => void;
|
|
onDelete?: () => void;
|
|
sourceUrl?: string;
|
|
onSourceUrlChange?: (value: string) => void;
|
|
sourceUrlPlaceholder?: string;
|
|
}
|
|
|
|
const ACCEPT = "image/jpeg,image/png,image/webp";
|
|
|
|
export function ImageUploadField({
|
|
label,
|
|
currentUrl,
|
|
file,
|
|
onFileChange,
|
|
onDelete,
|
|
sourceUrl,
|
|
onSourceUrlChange,
|
|
sourceUrlPlaceholder = "https://example.com/image.jpg",
|
|
}: ImageUploadFieldProps) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const previewUrl = file ? URL.createObjectURL(file) : currentUrl;
|
|
|
|
const handleDrop = useCallback(
|
|
(event: React.DragEvent) => {
|
|
event.preventDefault();
|
|
const dropped = event.dataTransfer.files[0];
|
|
if (dropped && ACCEPT.split(",").includes(dropped.type)) {
|
|
onFileChange(dropped);
|
|
}
|
|
},
|
|
[onFileChange],
|
|
);
|
|
|
|
const handleDragOver = useCallback((event: React.DragEvent) => {
|
|
event.preventDefault();
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label>{label}</Label>
|
|
{previewUrl ? (
|
|
<div className="relative">
|
|
<img
|
|
src={previewUrl}
|
|
alt={label}
|
|
className="bg-muted h-32 w-full rounded-lg border object-cover"
|
|
/>
|
|
<div className="absolute top-1 right-1 flex gap-1">
|
|
{file && (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={() => onFileChange(null)}
|
|
title="Remove selected file"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
{!file && currentUrl && onDelete && (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
size="icon"
|
|
className="h-6 w-6"
|
|
onClick={onDelete}
|
|
title="Delete image"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => inputRef.current?.click()}
|
|
onDrop={handleDrop}
|
|
onDragOver={handleDragOver}
|
|
className="border-border text-muted-foreground hover:border-primary hover:bg-accent flex h-32 w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-colors"
|
|
>
|
|
<Upload className="h-6 w-6" />
|
|
<span className="text-xs">Click or drop image</span>
|
|
</button>
|
|
)}
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept={ACCEPT}
|
|
className="hidden"
|
|
onChange={(event) => {
|
|
const selected = event.target.files?.[0];
|
|
if (selected) onFileChange(selected);
|
|
event.target.value = "";
|
|
}}
|
|
/>
|
|
{onSourceUrlChange ? (
|
|
<div className="space-y-2">
|
|
<Input
|
|
type="url"
|
|
value={sourceUrl ?? ""}
|
|
onChange={(event) => onSourceUrlChange(event.target.value)}
|
|
placeholder={sourceUrlPlaceholder}
|
|
/>
|
|
<p className="text-muted-foreground text-xs">
|
|
Paste an image URL instead of uploading a file. If both are provided, the uploaded file
|
|
wins.
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|