"use client"; import { useActionState, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import dynamic from "next/dynamic"; import { Download, Eye, FileText, ImageIcon, Layers, Loader2, Trash2, Upload } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Progress } from "@/components/ui/progress"; import { deleteJobFileAction } from "@/lib/appwrite/job-file-actions"; import { initialJobFileActionState, JOB_FILE_KIND_LABELS, } from "@/lib/appwrite/job-file-types"; import type { JobFileWithUrl } from "@/lib/appwrite/job-file-queries"; // three.js + react-three is ~500KB minified; only load it when the user // actually opens the viewer dialog. const STLViewer = dynamic( () => import("@/components/stl-viewer").then((m) => m.STLViewer), { ssr: false, loading: () => null }, ); const VIEWABLE_RE = /\.(stl|ply|obj)$/i; function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } function kindIcon(kind: string) { if (kind === "image") return ; if (kind === "scan") return ; return ; } export function JobFilesPanel({ jobId, files, }: { jobId: string; files: JobFileWithUrl[]; }) { return (
{files.length === 0 ? (

Henüz dosya yok.

) : (
    {files.map((f) => ( ))}
)}
); } const MAX_FILE_BYTES = 200 * 1024 * 1024; const MAX_BATCH_BYTES = 200 * 1024 * 1024; // proxy cap; one large file fills the whole batch type UploadPhase = "idle" | "uploading" | "processing"; function UploadForm({ jobId }: { jobId: string }) { const router = useRouter(); const inputRef = useRef(null); const xhrRef = useRef(null); const [selected, setSelected] = useState([]); const [phase, setPhase] = useState("idle"); const [progress, setProgress] = useState(0); // 0–100 const totalBytes = selected.reduce((s, f) => s + f.size, 0); const overSize = selected.find((f) => f.size > MAX_FILE_BYTES); const overBatch = totalBytes > MAX_BATCH_BYTES; const blocked = Boolean(overSize) || overBatch; const busy = phase !== "idle"; function reset() { setSelected([]); setProgress(0); setPhase("idle"); if (inputRef.current) inputRef.current.value = ""; } function startUpload() { if (selected.length === 0 || blocked) return; const formData = new FormData(); for (const f of selected) formData.append("files", f); const xhr = new XMLHttpRequest(); xhrRef.current = xhr; setPhase("uploading"); setProgress(0); xhr.upload.addEventListener("progress", (e) => { if (!e.lengthComputable) return; const pct = Math.round((e.loaded / e.total) * 100); setProgress(pct); }); // The browser has finished pushing bytes; the server is now writing to // Appwrite. Flip to "processing" so the user sees something is still // happening (large files take 30-60s to land in Storage). xhr.upload.addEventListener("load", () => { setPhase("processing"); setProgress(100); }); xhr.addEventListener("load", () => { let payload: { ok?: boolean; uploaded?: number; error?: string } = {}; try { payload = JSON.parse(xhr.responseText); } catch { /* non-JSON response */ } if (xhr.status >= 200 && xhr.status < 300 && payload.ok) { toast.success(`${payload.uploaded ?? selected.length} dosya yüklendi.`); reset(); router.refresh(); } else { toast.error(payload.error || `Yükleme başarısız (HTTP ${xhr.status}).`); setPhase("idle"); setProgress(0); } }); xhr.addEventListener("error", () => { setPhase("idle"); setProgress(0); toast.error("Ağ hatası. Tekrar deneyin."); }); xhr.addEventListener("abort", () => { setPhase("idle"); setProgress(0); toast.message("Yükleme iptal edildi."); }); xhr.open("POST", `/api/jobs/${jobId}/files`); xhr.send(formData); } function cancelUpload() { xhrRef.current?.abort(); } return (
{ const list = e.target.files ? Array.from(e.target.files) : []; setSelected(list); setProgress(0); }} /> {selected.length > 0 ? ( overSize ? ( {overSize.name} 200MB'tan büyük (her dosya maksimum 200MB). ) : overBatch ? ( Toplam {formatSize(totalBytes)} — 200MB sınırını aşıyor. Daha az dosya seçin. ) : ( <> {selected.length} dosya seçildi ({formatSize(totalBytes)}) ) ) : ( "Tarama (STL/OBJ), görsel veya PDF — max 200MB / dosya" )} {phase === "uploading" ? ( ) : phase === "processing" ? ( ) : ( )}
{phase === "uploading" && (
{progress}%
)} {phase === "processing" && (
Sunucu Appwrite'a yazıyor — büyük dosyalar 30-60 sn sürebilir
)}
); } function FileRow({ file }: { file: JobFileWithUrl }) { const [state, action, pending] = useActionState( deleteJobFileAction, initialJobFileActionState, ); const router = useRouter(); const [open, setOpen] = useState(false); const [downloadOpen, setDownloadOpen] = useState(false); const [viewerOpen, setViewerOpen] = useState(false); const isArchived = Boolean(file.archivedAt); const isViewable = !isArchived && VIEWABLE_RE.test(file.name); useEffect(() => { if (state.ok) { toast.success("Dosya silindi."); setOpen(false); router.refresh(); } else if (state.error) { toast.error(state.error); } }, [state, router]); function triggerDownload() { // Use a programmatic anchor click — the server route streams the file // with Content-Disposition: attachment, so the browser hands it straight // to the download manager. Toast confirms it left our side. const a = document.createElement("a"); a.href = file.url; a.download = file.name; document.body.appendChild(a); a.click(); a.remove(); setDownloadOpen(false); toast.success("İndirme başladı.", { description: file.name }); } return (
  • {kindIcon(file.kind)}

    {file.name}

    {JOB_FILE_KIND_LABELS[file.kind] ?? file.kind} · {formatSize(file.size)} {isArchived && ( <> {" · "} Arşivlendi {new Date(file.archivedAt!).toLocaleDateString("tr-TR")} )}

    {JOB_FILE_KIND_LABELS[file.kind] ?? file.kind} {isViewable && ( {file.name} {formatSize(file.size)} · Sürükleyerek döndürün, kaydırarak yaklaşın.
    {viewerOpen && }
    )} Dosya indirilsin mi? {file.name} · {formatSize(file.size)} Dosya silinsin mi? {file.name}
  • ); }