Refactor and implement submit form functionality

This commit is contained in:
Jared Schoeny
2025-10-15 13:57:26 -10:00
parent ef141ad1fa
commit e3f4304525
15 changed files with 1660 additions and 462 deletions

View File

@@ -1,35 +1,103 @@
"use client";
import React from "react";
import { hacks as allHacks } from "@/data/hacks";
import HackCard from "@/components/HackCard";
import { createClient } from "@/utils/supabase/client";
export default function DiscoverBrowser() {
const [query, setQuery] = React.useState("");
const [tag, setTag] = React.useState<string | null>(null);
const [sort, setSort] = React.useState("popular");
const [hacks, setHacks] = React.useState<any[]>([]);
const [tags, setTags] = React.useState<string[]>([]);
const tags = React.useMemo(() => {
const set = new Set<string>();
allHacks.forEach((h) => h.tags.forEach((t) => set.add(t)));
return Array.from(set).sort();
}, []);
React.useEffect(() => {
const run = async () => {
const supabase = createClient();
const { data: rows } = await supabase
.from("hacks")
.select("slug,title,summary,description,base_rom,version,downloads,created_by,patch_url")
.order(sort === "popular" ? "downloads" : "created_at", { ascending: false });
const slugs = (rows || []).map((r) => r.slug);
const { data: coverRows } = await supabase
.from("hack_covers")
.select("hack_slug,url,position")
.in("hack_slug", slugs)
.order("position", { ascending: true });
const coversBySlug = new Map<string, string[]>();
if (coverRows && coverRows.length > 0) {
const { data: imagesData } = await supabase.storage
.from('hack-covers')
.createSignedUrls(coverRows.map(c => c.url), 60 * 5);
if (imagesData) {
// Map: storage object url -> signedUrl
const urlToSignedUrl = new Map<string, string>();
imagesData.forEach((d, idx) => {
// If creation fails, d.signedUrl might be undefined; filter those out
if (d.signedUrl) urlToSignedUrl.set(coverRows[idx].url, d.signedUrl);
});
const hacks = React.useMemo(() => {
let filtered = allHacks.filter((h) => {
const q = query.toLowerCase();
const matchesQ =
!q ||
coverRows.forEach((c) => {
const arr = coversBySlug.get(c.hack_slug) || [];
const signed = urlToSignedUrl.get(c.url);
if (signed) {
arr.push(signed);
coversBySlug.set(c.hack_slug, arr);
}
});
}
}
const { data: tagRows } = await supabase
.from("hack_tags")
.select("hack_slug,tags(name)")
.in("hack_slug", slugs);
const tagsBySlug = new Map<string, string[]>();
(tagRows || []).forEach((r: any) => {
if (!r.tags?.name) return;
const arr = tagsBySlug.get(r.hack_slug) || [];
arr.push(r.tags.name);
tagsBySlug.set(r.hack_slug, arr);
});
const { data: profiles } = await supabase
.from("profiles")
.select("id,username");
const usernameById = new Map<string, string>();
(profiles || []).forEach((p) => usernameById.set(p.id, p.username ? `@${p.username}` : "Unknown"));
const mapped = (rows || []).map((r) => ({
slug: r.slug,
title: r.title,
author: usernameById.get(r.created_by as string) || "Unknown",
covers: coversBySlug.get(r.slug) || [],
tags: tagsBySlug.get(r.slug) || [],
downloads: r.downloads,
baseRomId: r.base_rom,
version: r.version,
summary: r.summary,
description: r.description,
}));
setHacks(mapped);
const tagSet = new Set<string>();
mapped.forEach((h) => h.tags.forEach((t) => tagSet.add(t)));
setTags(Array.from(tagSet).sort());
};
run();
}, [sort]);
const filtered = React.useMemo(() => {
let out = hacks;
const q = query.toLowerCase();
if (q) {
out = out.filter((h) =>
h.title.toLowerCase().includes(q) ||
h.author.toLowerCase().includes(q) ||
h.description.toLowerCase().includes(q);
const matchesTag = !tag || h.tags.includes(tag);
return matchesQ && matchesTag;
});
if (sort === "popular") filtered = filtered.sort((a, b) => b.downloads - a.downloads);
if (sort === "new") filtered = filtered; // mock
return filtered;
}, [query, tag, sort]);
(h.description || "").toLowerCase().includes(q)
);
}
if (tag) out = out.filter((h) => h.tags.includes(tag));
return out;
}, [hacks, query, tag]);
return (
<div>
@@ -79,7 +147,7 @@ export default function DiscoverBrowser() {
</div>
<div className="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{hacks.map((hack) => (
{filtered.map((hack) => (
<HackCard key={hack.slug} hack={hack} />
))}
</div>

View File

@@ -1,18 +1,33 @@
import Image from "next/image";
import PixelImage from "./PixelImage";
import Link from "next/link";
import { formatCompactNumber } from "@/utils/format";
import { useBaseRoms } from "@/contexts/BaseRomContext";
import type { Hack } from "@/data/hacks";
import { baseRoms } from "@/data/baseRoms";
import { useEffect, useState } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { usePathname } from "next/navigation";
import { FaRegImages } from "react-icons/fa6";
// Using shared Hack type from data
type CardHack = {
slug: string;
title: string;
author: string;
covers: string[];
tags: string[];
downloads: number;
baseRomId?: string;
version: string;
summary?: string;
description?: string;
};
export default function HackCard({ hack, clickable = true, className = "" }: { hack: Hack; clickable?: boolean; className?: string }) {
export default function HackCard({ hack, clickable = true, className = "" }: { hack: CardHack; clickable?: boolean; className?: string }) {
const { isLinked, hasPermission, hasCached } = useBaseRoms();
const linked = isLinked(hack.baseRom);
const ready = hasPermission(hack.baseRom) || hasCached(hack.baseRom);
const match = baseRoms.find((r) => r.id === hack.baseRomId);
const baseName = match?.name ?? undefined;
const linked = baseName ? isLinked(baseName) : false;
const ready = baseName ? hasPermission(baseName) || hasCached(baseName) : false;
const images = (hack.covers && hack.covers.length > 0 ? hack.covers : []).filter(Boolean);
const isCarousel = images.length > 1;
const pathname = usePathname();
@@ -30,7 +45,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
emblaApi.off("select", onSelect);
};
}, [emblaApi]);
const cardClass = `rounded-[12px] overflow-hidden ${
const cardClass = `rounded-[12px] overflow-hidden min-w-[360px] ${
clickable ? "transition-transform duration-300 hover:-translate-y-0.5 hover:shadow-xl anim-float" : ""
} ring-1 ${ready ? "ring-emerald-400/50 bg-emerald-500/10" : "card ring-[var(--border)]"}`;
const gradientBgClass = `bg-gradient-to-b ${ready ? 'from-emerald-300/5 to-emerald-400/30 dark:from-emerald-950/10 dark:to-emerald-600/40' : 'from-black/30 to-black/10 dark:from-black/80 dark:to-black/40'}`;
@@ -75,7 +90,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
{hack.tags.slice(0, 2).map((t) => (
<span
key={t}
className="rounded-full px-2 py-0.5 text-xs ring-1 ring-[var(--border)] bg-[var(--surface-2)] text-foreground/90 backdrop-blur-md"
className="rounded-full px-2 py-0.5 text-xs ring-1 ring-foreground/20 dark:ring-foreground/30 bg-background/70 text-foreground/90 backdrop-blur-md"
>
{t}
</span>
@@ -141,7 +156,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
return text.length > 120 ? text.slice(0, 120).trimEnd() + "…" : text;
})()}
</p>
<div className="mt-3 text-xs text-foreground/60">Base: {hack.baseRom}</div>
<div className="mt-3 text-xs text-foreground/60">Base: {baseName ?? "Unknown"}</div>
</div>
</div>
);

File diff suppressed because it is too large Load Diff