Files
twotalesanimation 7b36329769
Deploy / deploy (push) Successful in 2m43s
Hide Archived Productions
2026-08-06 10:42:49 +02:00

124 lines
4.0 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";
import { HIDE_ARCHIVED_KEY } from "@/components/settings/HideArchivedToggle";
export default function ProjectsPage() {
const [search, setSearch] = useState("");
const [showNew, setShowNew] = useState(false);
const [hideArchived, setHideArchived] = useState(false);
useEffect(() => {
setHideArchived(localStorage.getItem(HIDE_ARCHIVED_KEY) === "1");
}, []);
const { data, isLoading } = 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 visibleProjects = hideArchived ? projects.filter((p: any) => p.status !== "ARCHIVED") : projects;
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" : ""}
{hideArchived && projects.length !== visibleProjects.length && (
<span className="text-zinc-600 ml-1">({projects.length - visibleProjects.length} archived hidden)</span>
)}
</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>
) : 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">
{visibleProjects.map((project: any) => (
<div key={project.id} onClick={saveScroll}>
<ProjectCard project={project} />
</div>
))}
</div>
)}
<NewProjectDialog
open={showNew}
onClose={() => setShowNew(false)}
clients={clients}
/>
</div>
);
}