"use client"; import { useEffect, useState, useMemo } from "react"; import { useSearchParams } from "next/navigation"; import Link from "next/link"; import Image from "next/image"; type ProjectRow = { id?: string | number; submission_id?: string | number; title?: string; uploader?: string; category?: string; tags?: string[] | string | null; p_image?: { filename_disk?: string; title?: string } | null; }; export default function ProjectsPage() { const searchParams = useSearchParams(); const initialQuery = searchParams.get("query") || ""; const [query, setQuery] = useState(initialQuery); const [debouncedQuery, setDebouncedQuery] = useState(initialQuery); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [categories] = useState([ "assets", "documents", "fixtures", "projects", "templates", "test files", "tools", ]); // simple debounce useEffect(() => { const timer = setTimeout(() => setDebouncedQuery(query), 300); return () => clearTimeout(timer); }, [query]); // fetch EXACTLY like the working pre-patch version useEffect(() => { const url = new URL(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/projects`); url.searchParams.set( "fields", "submission_id,title,uploader,category,tags,p_image.filename_disk,p_image.title" ); url.searchParams.set("limit", "-1"); fetch(url.toString(), { cache: "no-store" }) .then((res) => res.json()) .then((data) => { const list = Array.isArray(data?.data) ? (data.data as ProjectRow[]) : []; setProjects(list); setLoading(false); }) .catch(() => setLoading(false)); }, []); const highlight = (text: string) => { if (!debouncedQuery) return text; const regex = new RegExp(`(${debouncedQuery})`, "gi"); return text?.replace(regex, "$1"); }; const normalize = (str: unknown) => String(str ?? "").toLowerCase().replace(/[_\s]/g, ""); const filtered = useMemo(() => { const q = normalize(debouncedQuery); return projects.filter((entry) => { const tags = Array.isArray(entry.tags) ? entry.tags : typeof entry.tags === "string" ? entry.tags.split(",").map((t) => t.trim()).filter(Boolean) : []; const fieldsToSearch = [ entry.title ?? "", entry.uploader ?? "", entry.category ?? "", tags.join(" "), ]; return fieldsToSearch .filter(Boolean) .some((field) => normalize(field).includes(q)); }); }, [projects, debouncedQuery]); const tagCounts = useMemo(() => { const counts: Record = {}; projects.forEach((p) => { const tags = Array.isArray(p.tags) ? p.tags : typeof p.tags === "string" ? p.tags.split(",").map((t) => t.trim()).filter(Boolean) : []; tags.forEach((tag) => { counts[tag] = (counts[tag] || 0) + 1; }); }); return counts; }, [projects]); const popularTags = Object.entries(tagCounts) .sort((a, b) => (Number(b[1]) || 0) - (Number(a[1]) || 0)) .slice(0, 10) .map(([tag]) => tag); const recentTags = [...projects] .sort( (a, b) => Number(b.submission_id ?? 0) - Number(a.submission_id ?? 0) ) .slice(0, 10) .flatMap((p) => { const tags = Array.isArray(p.tags) ? p.tags : typeof p.tags === "string" ? p.tags.split(",").map((t) => t.trim()).filter(Boolean) : []; return tags; }) .filter((tag, i, self) => self.indexOf(tag) === i) .slice(0, 10); const uniqueUploaders = new Set( projects.map((p) => p.uploader).filter(Boolean) ).size; const totalTags = Object.keys(tagCounts).length; const detailHref = (p: ProjectRow) => `/projects/${p.submission_id}`; const imageSrc = (p: ProjectRow) => p.p_image?.filename_disk ? `https://forms.lasereverything.net/assets/${p.p_image.filename_disk}` : null; return (

Community Projects

setQuery(e.target.value)} placeholder="Search projects..." className="w-full dark:bg-background border border-border rounded-md p-2" /> ← Back to Main Menu

Popular Tags

{popularTags.length > 0 ? (
{popularTags.map((tag, i) => ( setQuery(tag)} title={tag}> {tag} ))}
) : (

No tags yet.

)}

Recent Tags

{recentTags.length > 0 ? (
{recentTags.map((tag, i) => ( setQuery(tag)} title={tag}> {tag} ))}
) : (

No recent tags.

)}

Project Stats

Total Projects: {projects.length}

Unique Uploaders: {uniqueUploaders}

Total Tags: {totalTags}

Browse by Category

{categories.map((cat, i) => ( setQuery(cat)}> {cat === "test_files" ? "test files" : cat} ))}

Submit a Project

Have a cool design, tool, or jig to share? Submit it to the community database.


{loading ? (

Loading projects...

) : filtered.length === 0 ? (

No projects found.

) : (
{filtered.map((project) => { const key = String(project.submission_id ?? project.id ?? Math.random()); const href = detailHref(project); const src = imageSrc(project); return (
{src ? ( {project.p_image?.title ) : (
No image
)}

Uploaded by:{" "}

Category:{" "}

{(() => { const tags = Array.isArray(project.tags) ? project.tags : typeof project.tags === "string" ? project.tags.split(",").map((t) => t.trim()).filter(Boolean) : []; return tags.length ? tags.map((tag, i) => ( setQuery(tag)} title={tag}> {tag} )) : ""; })()}
); })}
)}
); }