Files
vfxreview/app/(dashboard)/projects/page.tsx
T
twotalesanimation 404daa081e
Deploy / deploy (push) Successful in 3m12s
oops2
2026-08-01 17:51:33 +02:00

118 lines
3.7 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ProjectCard } from "@/components/projects/ProjectCard";
import { NewProjectDialog } from "@/components/projects/NewProjectDialog";
import { Plus, Search, Loader2 } from "lucide-react";
export default function ProjectsPage() {
const [search, setSearch] = useState("");
const [showNew, setShowNew] = useState(false);
const { data, isLoading, isError } = useQuery({
queryKey: ["projects", search],
queryFn: async () => {
const res = await fetch(`/api/projects?q=${encodeURIComponent(search)}`);
if (!res.ok) throw new Error("Failed to fetch projects");
return res.json() as Promise<{ projects: any[] }>;
},
staleTime: 30_000,
});
const { data: clientsData } = useQuery({
queryKey: ["clients"],
queryFn: async () => {
const res = await fetch("/api/clients");
if (!res.ok) return { clients: [] };
return res.json() as Promise<{ clients: { id: string; company: string }[] }>;
},
staleTime: 60_000,
});
const projects = data?.projects ?? [];
const clients = clientsData?.clients ?? [];
const SCROLL_KEY = 'projects-scroll';
useEffect(() => {
if (!isLoading) {
const saved = sessionStorage.getItem(SCROLL_KEY);
if (saved) {
sessionStorage.removeItem(SCROLL_KEY);
requestAnimationFrame(() => window.scrollTo(0, parseInt(saved, 10)));
}
}
}, [isLoading]);
const saveScroll = () => sessionStorage.setItem(SCROLL_KEY, String(window.scrollY));
return (
<div className="p-8 space-y-6 max-w-[1600px] mx-auto">
<div className="flex items-center justify-between gap-4 mb-8">
<div>
<h1 className="text-3xl font-bold text-white">Projects</h1>
<p className="text-zinc-400 mt-1">
{projects.length} project{projects.length !== 1 ? "s" : ""}
</p>
</div>
<Button onClick={() => setShowNew(true)} className="gap-2">
<Plus className="h-4 w-4" />
New Project
</Button>
</div>
{/* Search */}
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search projects..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
{/* Grid */}
{isLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : isError ? (
<div className="text-center py-12 text-muted-foreground">
<p>Could not load projects.</p>
<p className="text-xs mt-2">Check server logs and database connectivity, then refresh.</p>
</div>
) : projects.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<p>No projects found.</p>
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => setShowNew(true)}
>
Create your first project
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{projects.map((project) => (
<div key={project.id} onClick={saveScroll}>
<ProjectCard project={project} />
</div>
))}
</div>
)}
<NewProjectDialog
open={showNew}
onClose={() => setShowNew(false)}
clients={clients}
/>
</div>
);
}