|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
|
import { useAuth } from "@/lib/auth-context";
|
|
|
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Pagination,
|
|
|
|
|
|
PaginationContent,
|
|
|
|
|
|
PaginationItem,
|
|
|
|
|
|
PaginationLink,
|
|
|
|
|
|
PaginationPrevious,
|
|
|
|
|
|
PaginationNext,
|
|
|
|
|
|
} from "@/components/ui/pagination";
|
|
|
|
|
|
|
|
|
|
|
|
interface UploadedFile {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
original_name: string;
|
|
|
|
|
|
file_path: string;
|
|
|
|
|
|
upload_status: string;
|
|
|
|
|
|
created_at: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function RendersPage() {
|
|
|
|
|
|
const { isAuthenticated, user } = useAuth();
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const [files, setFiles] = useState<UploadedFile[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [authInitialized, setAuthInitialized] = useState(false);
|
|
|
|
|
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
|
|
|
|
const [page, setPage] = useState(1);
|
|
|
|
|
|
const pageSize = 5;
|
|
|
|
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const timer = setTimeout(() => setAuthInitialized(true), 100);
|
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!authInitialized) return;
|
|
|
|
|
|
if (!isAuthenticated || !user) {
|
|
|
|
|
|
router.push("/login?redirect=/renders");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
fetchFiles(page);
|
|
|
|
|
|
// eslint-disable-next-line
|
|
|
|
|
|
}, [authInitialized, isAuthenticated, user, page]);
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchFiles(pageNum: number) {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
|
`/api/renders/in?page=${pageNum}&pageSize=${pageSize}`,
|
|
|
|
|
|
{
|
|
|
|
|
|
headers: { "user-id": user?.id || "" },
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
setFiles(data.files || []);
|
|
|
|
|
|
setTotalPages(data.pagination?.totalPages || 1);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setFiles([]);
|
|
|
|
|
|
setTotalPages(1);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleDelete(id: string) {
|
|
|
|
|
|
if (!user) return;
|
|
|
|
|
|
const ok = confirm("Delete this file? This cannot be undone.");
|
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
|
setDeletingId(id);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch("/api/renders/in", {
|
|
|
|
|
|
method: "DELETE",
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
"user-id": user.id,
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ id }),
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (!res.ok) throw new Error(data?.error || "Delete failed");
|
|
|
|
|
|
await fetchFiles(page);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert((e as any).message || "Delete failed");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setDeletingId(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!authInitialized || !isAuthenticated) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
|
|
|
|
Loading...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const canPrev = page > 1;
|
|
|
|
|
|
const canNext = page < totalPages;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-50 py-6 sm:py-12">
|
|
|
|
|
|
<div className="max-w-5xl mx-auto px-4 sm:px-6">
|
|
|
|
|
|
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 mb-6 sm:mb-8">
|
|
|
|
|
|
My Renders
|
|
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="mb-8 sm:mb-12">
|
|
|
|
|
|
<h2 className="text-xl sm:text-2xl font-bold text-orange-600 mb-4">
|
|
|
|
|
|
Uploaded Files
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div>Loading...</div>
|
|
|
|
|
|
) : files.length === 0 ? (
|
|
|
|
|
|
<div className="text-gray-500">No files uploaded yet.</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="overflow-x-auto -mx-4 sm:mx-0">
|
|
|
|
|
|
<div className="inline-block min-w-full align-middle">
|
|
|
|
|
|
<table className="min-w-full bg-white rounded-lg shadow-md">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr className="bg-orange-50">
|
|
|
|
|
|
<th className="p-2 sm:p-3 text-left font-semibold text-xs sm:text-sm">
|
|
|
|
|
|
Filename
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="p-2 sm:p-3 text-left font-semibold text-xs sm:text-sm">
|
|
|
|
|
|
Status
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="p-2 sm:p-3 text-left font-semibold text-xs sm:text-sm hidden sm:table-cell">
|
|
|
|
|
|
Uploaded At
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="p-2 sm:p-3 text-left font-semibold text-xs sm:text-sm">
|
|
|
|
|
|
Download
|
|
|
|
|
|
</th>
|
|
|
|
|
|
<th className="p-2 sm:p-3 text-left font-semibold text-xs sm:text-sm">
|
|
|
|
|
|
Actions
|
|
|
|
|
|
</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{files.map((f) => (
|
|
|
|
|
|
<tr key={f.id} className="border-t">
|
|
|
|
|
|
<td className="p-2 sm:p-3 text-xs sm:text-sm break-words max-w-[120px] sm:max-w-none">
|
|
|
|
|
|
{f.original_name}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="p-2 sm:p-3 capitalize">
|
|
|
|
|
|
{/* Mobile: Green checkmark */}
|
|
|
|
|
|
{f.upload_status === "COMPLETE" && (
|
|
|
|
|
|
<span className="sm:hidden text-green-500 text-xl">
|
|
|
|
|
|
✓
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{f.upload_status !== "COMPLETE" && (
|
|
|
|
|
|
<span className="sm:hidden text-gray-400 text-xl">
|
|
|
|
|
|
○
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* Desktop: Text badge */}
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`hidden sm:inline-block px-2 py-1 rounded-full text-xs font-bold ${
|
|
|
|
|
|
f.upload_status === "COMPLETE"
|
|
|
|
|
|
? "bg-green-100 text-green-800"
|
|
|
|
|
|
: "bg-gray-200 text-gray-600"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{f.upload_status}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="p-2 sm:p-3 text-xs sm:text-sm hidden sm:table-cell">
|
|
|
|
|
|
{new Date(f.created_at).toLocaleString()}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="p-2 sm:p-3">
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={
|
|
|
|
|
|
f.file_path
|
|
|
|
|
|
? `/uploads/${f.file_path.split("/").pop()}`
|
|
|
|
|
|
: "#"
|
|
|
|
|
|
}
|
|
|
|
|
|
className="underline text-blue-700 text-xs sm:text-sm"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
|
download={f.original_name}
|
|
|
|
|
|
>
|
|
|
|
|
|
Download
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td className="p-2 sm:p-3">
|
|
|
|
|
|
{/* Mobile: Red X icon - circular */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="sm:hidden w-6 h-6 rounded-full bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300 disabled:opacity-50 flex items-center justify-center"
|
|
|
|
|
|
disabled={deletingId === f.id}
|
|
|
|
|
|
onClick={() => handleDelete(f.id)}
|
|
|
|
|
|
aria-label="Delete"
|
|
|
|
|
|
>
|
|
|
|
|
|
{deletingId === f.id ? (
|
|
|
|
|
|
<span className="text-xs">...</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-sm font-bold">×</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{/* Desktop: Text button */}
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="hidden sm:inline-block px-3 py-1 text-sm rounded-md bg-red-500 text-white hover:bg-red-600 disabled:bg-red-300"
|
|
|
|
|
|
disabled={deletingId === f.id}
|
|
|
|
|
|
onClick={() => handleDelete(f.id)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{deletingId === f.id ? "Deleting..." : "Delete"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-4 flex justify-center">
|
|
|
|
|
|
<Pagination>
|
|
|
|
|
|
<PaginationContent>
|
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
|
<PaginationPrevious
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (canPrev) setPage(page - 1);
|
|
|
|
|
|
}}
|
|
|
|
|
|
aria-disabled={!canPrev}
|
|
|
|
|
|
className={
|
|
|
|
|
|
!canPrev ? "pointer-events-none opacity-50" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
|
|
|
|
|
(p) => (
|
|
|
|
|
|
<PaginationItem key={p}>
|
|
|
|
|
|
<PaginationLink
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
isActive={p === page}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setPage(p);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{p}
|
|
|
|
|
|
</PaginationLink>
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
)
|
|
|
|
|
|
)}
|
|
|
|
|
|
<PaginationItem>
|
|
|
|
|
|
<PaginationNext
|
|
|
|
|
|
href="#"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (canNext) setPage(page + 1);
|
|
|
|
|
|
}}
|
|
|
|
|
|
aria-disabled={!canNext}
|
|
|
|
|
|
className={
|
|
|
|
|
|
!canNext ? "pointer-events-none opacity-50" : ""
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</PaginationItem>
|
|
|
|
|
|
</PaginationContent>
|
|
|
|
|
|
</Pagination>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
<h2 className="text-xl sm:text-2xl font-bold text-gray-600 mb-4">
|
|
|
|
|
|
Rendered Files (outputs)
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 sm:p-8 text-center text-gray-400 text-sm sm:text-base">
|
|
|
|
|
|
Placeholder: Your rendered products will appear here in the future!
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|