Files
hosting-files/src/components/UploadZone.tsx
T
orohi 8bb436ea4f first commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 14:59:02 +03:00

201 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useRef, useState } from "react";
import {
Upload,
ImageIcon,
Link2,
Check,
Loader2,
X,
Copy,
} from "lucide-react";
interface UploadedPhoto {
id: string;
url: string;
originalName: string;
}
export function UploadZone() {
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [uploaded, setUploaded] = useState<UploadedPhoto | null>(null);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const uploadFile = useCallback(async (file: File) => {
if (!file.type.startsWith("image/")) {
setError("Можно загружать только изображения");
return;
}
if (file.size > 10 * 1024 * 1024) {
setError("Максимальный размер файла — 10 МБ");
return;
}
setError(null);
setIsUploading(true);
setUploaded(null);
const formData = new FormData();
formData.append("file", file);
try {
const res = await fetch("/api/upload", {
method: "POST",
body: formData,
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Ошибка загрузки");
}
const photo = await res.json();
setUploaded(photo);
window.dispatchEvent(new CustomEvent("photo-uploaded", { detail: photo }));
} catch (err) {
setError(err instanceof Error ? err.message : "Ошибка загрузки");
} finally {
setIsUploading(false);
}
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file) uploadFile(file);
},
[uploadFile]
);
const handleCopy = async () => {
if (!uploaded) return;
const fullUrl = `${window.location.origin}${uploaded.url}`;
await navigator.clipboard.writeText(fullUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const reset = () => {
setUploaded(null);
setError(null);
setCopied(false);
};
return (
<section id="upload" className="px-6 py-16">
<div className="mx-auto max-w-2xl">
<div
onDragOver={(e) => {
e.preventDefault();
setIsDragging(true);
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
onClick={() => !isUploading && !uploaded && inputRef.current?.click()}
className={`group relative cursor-pointer rounded-3xl border-2 border-dashed p-12 text-center transition-all duration-300 ${
isDragging
? "border-indigo-400 bg-indigo-500/10 glow"
: "border-border hover:border-indigo-500/50 hover:bg-surface/50"
} ${uploaded ? "cursor-default border-emerald-500/30 bg-emerald-500/5" : ""}`}
>
<input
ref={inputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) uploadFile(file);
e.target.value = "";
}}
/>
{isUploading ? (
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-12 w-12 animate-spin text-indigo-400" />
<p className="text-lg font-medium">Загрузка...</p>
</div>
) : uploaded ? (
<div className="flex flex-col items-center gap-5">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-emerald-500/20">
<Check className="h-8 w-8 text-emerald-400" />
</div>
<div>
<p className="text-lg font-medium text-emerald-400">
Фото загружено!
</p>
<p className="mt-1 text-sm text-muted">{uploaded.originalName}</p>
</div>
<div className="flex w-full max-w-md items-center gap-2 rounded-xl bg-surface p-3">
<Link2 className="h-4 w-4 shrink-0 text-muted" />
<input
readOnly
value={`${typeof window !== "undefined" ? window.location.origin : ""}${uploaded.url}`}
className="flex-1 bg-transparent text-sm outline-none"
/>
<button
onClick={(e) => {
e.stopPropagation();
handleCopy();
}}
className="shrink-0 rounded-lg bg-indigo-500/20 p-2 transition-colors hover:bg-indigo-500/30"
>
{copied ? (
<Check className="h-4 w-4 text-emerald-400" />
) : (
<Copy className="h-4 w-4 text-indigo-400" />
)}
</button>
</div>
<button
onClick={(e) => {
e.stopPropagation();
reset();
}}
className="text-sm text-muted transition-colors hover:text-foreground"
>
Загрузить ещё
</button>
</div>
) : (
<div className="flex flex-col items-center gap-4">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-indigo-500/10 transition-transform group-hover:scale-110">
<Upload className="h-8 w-8 text-indigo-400" />
</div>
<div>
<p className="text-lg font-medium">
Перетащите фото сюда
</p>
<p className="mt-1 text-sm text-muted">
или нажмите для выбора файла
</p>
</div>
<div className="mt-2 flex items-center gap-4 text-xs text-muted">
<span className="flex items-center gap-1">
<ImageIcon className="h-3.5 w-3.5" />
JPG, PNG, GIF, WebP
</span>
<span>до 10 МБ</span>
</div>
</div>
)}
</div>
{error && (
<div className="mt-4 flex items-center gap-2 rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-400">
<X className="h-4 w-4 shrink-0" />
{error}
</div>
)}
</div>
</section>
);
}