diff --git a/app/projects/page.tsx b/app/projects/page.tsx index adb2769a..59224387 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -6,13 +6,13 @@ import Link from "next/link"; import Image from "next/image"; type ProjectRow = { - id: string | number; + id?: string | number; submission_id?: string | number; title?: string; uploader?: string; category?: string; - tags?: string[]; - p_image?: { filename_disk?: string; title?: string }; + tags?: string[] | string | null; + p_image?: { filename_disk?: string; title?: string } | null; }; export default function ProjectsPage() { @@ -33,23 +33,26 @@ export default function ProjectsPage() { "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,id,title,uploader,category,tags,p_image.filename_disk,p_image.title" + "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) => { - setProjects((data?.data as ProjectRow[]) || []); + const list = Array.isArray(data?.data) ? (data.data as ProjectRow[]) : []; + setProjects(list); setLoading(false); }) .catch(() => setLoading(false)); @@ -61,29 +64,44 @@ export default function ProjectsPage() { return text?.replace(regex, "$1"); }; - const normalize = (str: unknown) => String(str ?? "").toLowerCase().replace(/[_\s]/g, ""); + 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 ?? "", - Array.isArray(entry.tags) ? entry.tags.join(" ") : "", + tags.join(" "), ]; - return fieldsToSearch.filter(Boolean).some((field) => normalize(field).includes(q)); + + return fieldsToSearch + .filter(Boolean) + .some((field) => normalize(field).includes(q)); }); }, [projects, debouncedQuery]); const tagCounts = useMemo(() => { const counts: Record = {}; projects.forEach((p) => { - if (Array.isArray(p.tags)) { - p.tags.forEach((tag) => { - counts[tag] = (counts[tag] || 0) + 1; - }); - } + 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]); @@ -96,17 +114,27 @@ export default function ProjectsPage() { const recentTags = [...projects] .sort( (a, b) => - Number(b.submission_id ?? b.id ?? 0) - Number(a.submission_id ?? a.id ?? 0) + Number(b.submission_id ?? 0) - Number(a.submission_id ?? 0) ) .slice(0, 10) - .flatMap((p) => p.tags || []) + .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 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 ?? p.id}`; + 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}` @@ -264,7 +292,7 @@ export default function ProjectsPage() { ) : (
{filtered.map((project) => { - const key = String(project.submission_id ?? project.id); + const key = String(project.submission_id ?? project.id ?? Math.random()); const href = detailHref(project); const src = imageSrc(project); return ( @@ -285,7 +313,6 @@ export default function ProjectsPage() {
- {/* highlight title if searching */}
- {Array.isArray(project.tags) && project.tags.length > 0 - ? project.tags.map((tag, i) => ( + {(() => { + 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} )) - : ""} -
-
-
+ : ""; + })()} + + + ); })}