"use client"; import { useEffect, useId, useRef, useState } from "react"; import { Upload, Image as ImageIcon, X, Loader2, Check, AlertCircle, Library, GripVertical, } from "lucide-react"; interface MediaFile { id: string; name: string; size: number; url: string; mimeType?: string; createdAt?: string; } type Mode = "single" | "multiple"; interface BaseProps { name: string; label: string; help?: string; accept?: string; maxSizeMB?: number; } interface SingleProps extends BaseProps { multiple?: false; defaultValue?: string | null; } interface MultiProps extends BaseProps { multiple: true; defaultValue?: string[] | null; } export function MediaPicker(props: SingleProps | MultiProps) { const mode: Mode = props.multiple ? "multiple" : "single"; const id = useId(); const initial: string[] = mode === "multiple" ? (props as MultiProps).defaultValue ?? [] : (props as SingleProps).defaultValue ? [(props as SingleProps).defaultValue as string] : []; const [values, setValues] = useState(initial); const [showLibrary, setShowLibrary] = useState(false); const [activeUploads, setActiveUploads] = useState< Array<{ id: string; name: string; progress: number; error?: string }> >([]); const [dragOver, setDragOver] = useState(false); const inputRef = useRef(null); // For multiple mode the hidden value is one URL per line (matches existing // textarea-based admin actions). For single mode it's just the URL. const hiddenValue = mode === "multiple" ? values.join("\n") : values[0] ?? ""; function addUrl(url: string) { if (mode === "single") setValues([url]); else setValues((v) => (v.includes(url) ? v : [...v, url])); } function removeAt(i: number) { setValues((v) => v.filter((_, idx) => idx !== i)); } function moveItem(from: number, to: number) { setValues((v) => { const copy = [...v]; const [moved] = copy.splice(from, 1); copy.splice(to, 0, moved); return copy; }); } async function uploadFile(file: File) { const maxBytes = (props.maxSizeMB ?? 10) * 1024 * 1024; if (file.size > maxBytes) { setActiveUploads((u) => [ ...u, { id: Math.random().toString(36), name: file.name, progress: 0, error: `Dosya çok büyük (max ${props.maxSizeMB ?? 10} MB)`, }, ]); return; } const uploadId = Math.random().toString(36); setActiveUploads((u) => [ ...u, { id: uploadId, name: file.name, progress: 0 }, ]); const fd = new FormData(); fd.append("file", file); try { const url = await uploadWithProgress(fd, (pct) => { setActiveUploads((u) => u.map((up) => (up.id === uploadId ? { ...up, progress: pct } : up)), ); }); addUrl(url); setActiveUploads((u) => u.map((up) => up.id === uploadId ? { ...up, progress: 100 } : up, ), ); // 2 saniye sonra başarılı upload'ı listeden kaldır setTimeout(() => { setActiveUploads((u) => u.filter((up) => up.id !== uploadId)); }, 2000); } catch (err) { const msg = err instanceof Error ? err.message : "Yükleme başarısız"; setActiveUploads((u) => u.map((up) => up.id === uploadId ? { ...up, progress: 0, error: msg } : up, ), ); } } function handleFiles(files: FileList | null) { if (!files) return; const arr = Array.from(files); if (mode === "single") { uploadFile(arr[0]); } else { arr.forEach(uploadFile); } } return (
{/* Drop zone */}
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }} onClick={() => inputRef.current?.click()} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current?.click(); } }} className={`mt-1.5 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed bg-white p-6 text-center transition ${ dragOver ? "border-[var(--sky)] bg-[var(--sky-50)]" : "border-[var(--border)] hover:border-[var(--sky)]" }`} > { handleFiles(e.target.files); e.target.value = ""; }} className="hidden" />

{mode === "multiple" ? "Sürükle-bırak veya dosyaları seç" : "Sürükle-bırak veya dosya seç"}

PNG, JPG, WEBP, SVG • max {props.maxSizeMB ?? 10} MB

{/* Active uploads */} {activeUploads.length > 0 && (
{activeUploads.map((u) => (
{u.error ? ( ) : u.progress === 100 ? ( ) : ( )} {u.name}
{u.error ? "Hata" : `${u.progress}%`}
{u.error ? (

{u.error}

) : (
)}
))}
)} {/* Selected previews */} {values.length > 0 && (
{values.map((url, i) => (
{/* eslint-disable-next-line @next/next/no-img-element */} {mode === "multiple" && values.length > 1 && (
{i > 0 && ( )}
)}
))}
)} {props.help && (

{props.help}

)} {showLibrary && ( setShowLibrary(false)} onPick={(url) => { addUrl(url); if (mode === "single") setShowLibrary(false); }} /> )}
); } function uploadWithProgress( fd: FormData, onProgress: (pct: number) => void, ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/admin/media/upload"); xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) { onProgress(Math.round((e.loaded / e.total) * 100)); } }); xhr.onload = () => { try { const data = JSON.parse(xhr.responseText); if (xhr.status >= 200 && xhr.status < 300 && data.url) { resolve(data.url); } else { reject(new Error(data.error || `HTTP ${xhr.status}`)); } } catch { reject(new Error("Geçersiz sunucu yanıtı")); } }; xhr.onerror = () => reject(new Error("Ağ hatası")); xhr.send(fd); }); } function MediaLibraryModal({ mode, selected, onClose, onPick, }: { mode: Mode; selected: string[]; onClose: () => void; onPick: (url: string) => void; }) { const [files, setFiles] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetch("/api/admin/media/list") .then((r) => r.json()) .then((data) => { if (data.error) setError(data.error); else setFiles(data.files || []); }) .catch(() => setError("Liste yüklenemedi")); }, []); return (
e.stopPropagation()} >

Medya kütüphanesi

{mode === "multiple" ? "Birden fazla görsel seçebilirsiniz" : "Bir görsel seçin"}

{error && (

{error}

)} {!files && !error && (
)} {files && files.length === 0 && (
Kütüphanede görsel yok. Önce yükleyin.
)} {files && files.length > 0 && (
{files.map((f) => { const isSelected = selected.includes(f.url); return ( ); })}
)}
); }