diff --git a/src/app/page.tsx b/src/app/page.tsx index 6db2763..d37c54b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import HackCard from "@/components/HackCard"; import Button from "@/components/Button"; import { sortOrderedTags } from "@/utils/format"; import { getCoverSignedUrls } from "@/app/hack/actions"; +import { HackCardAttributes } from "@/components/HackCard"; export const metadata: Metadata = { alternates: { @@ -27,7 +28,7 @@ export default async function Home() { .order("downloads", { ascending: false }) .limit(6); - let hackData: any[] = []; + let hackData: HackCardAttributes[] = []; if (popularHacks && popularHacks.length > 0) { const slugs = popularHacks.map((h) => h.slug); @@ -110,6 +111,7 @@ export default async function Home() { version: mappedVersions.get(r.slug) || "Pre-release", summary: r.summary, description: r.description, + isArchive: r.original_author != null && r.current_patch === null, })); } return ( diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx index 1c7788a..e1dc80a 100644 --- a/src/components/Discover/DiscoverBrowser.tsx +++ b/src/components/Discover/DiscoverBrowser.tsx @@ -13,6 +13,7 @@ import { CATEGORY_ICONS } from "@/components/Icons/tagCategories"; import { useBaseRoms } from "@/contexts/BaseRomContext"; import { sortOrderedTags, OrderedTag } from "@/utils/format"; import { getCoverSignedUrls } from "@/app/hack/actions"; +import { HackCardAttributes } from "@/components/HackCard"; export default function DiscoverBrowser() { @@ -21,7 +22,7 @@ export default function DiscoverBrowser() { const [selectedTags, setSelectedTags] = React.useState([]); const [selectedBaseRoms, setSelectedBaseRoms] = React.useState([]); const [sort, setSort] = React.useState("popular"); - const [hacks, setHacks] = React.useState([]); + const [hacks, setHacks] = React.useState([]); const [tagGroups, setTagGroups] = React.useState>({}); const [ungroupedTags, setUngroupedTags] = React.useState([]); const [loadingHacks, setLoadingHacks] = React.useState(true); @@ -46,19 +47,24 @@ export default function DiscoverBrowser() { const run = async () => { setLoadingHacks(true); setLoadingTags(true); - let orderBy: string | undefined = undefined; + let query = supabase + .from("hacks") + .select("slug,title,summary,description,base_rom,downloads,created_by,updated_at,current_patch,original_author"); + if (sort === "popular") { - orderBy = "downloads"; + // When sorting by popularity, always show non-archive hacks first. + // Archives are defined as rows where original_author IS NOT NULL and current_patch IS NULL, + // so ordering by current_patch with NULLS LAST effectively pushes archives to the end. + query = query + .order("downloads", { ascending: false }) + .order("current_patch", { ascending: false, nullsFirst: false }); } else if (sort === "updated") { - orderBy = "updated_at"; + query = query.order("updated_at", { ascending: false }); } else { - orderBy = "created_at"; + query = query.order("created_at", { ascending: false }); } - const { data: rows } = await supabase - .from("hacks") - .select("slug,title,summary,description,base_rom,downloads,created_by,updated_at,current_patch,original_author") - .order(orderBy, { ascending: false }); + const { data: rows } = await query; const slugs = (rows || []).map((r) => r.slug); const { data: coverRows } = await supabase .from("hack_covers") @@ -132,6 +138,7 @@ export default function DiscoverBrowser() { version: mappedVersions.get(r.slug) || "Pre-release", summary: r.summary, description: r.description, + isArchive: r.original_author != null && r.current_patch === null, })); setHacks(mapped); @@ -177,15 +184,15 @@ export default function DiscoverBrowser() { } // AND filter across selected tags: hack must include all selectedTags if (selectedTags.length > 0) { - out = out.filter((h) => selectedTags.every((t) => h.tags.includes(t))); + out = out.filter((h) => selectedTags.every((t) => h.tags.some((tag) => tag.name === t))); } // OR filter across base roms: hack's baseRomId must be in selectedBaseRoms if (selectedBaseRoms.length > 0) { - out = out.filter((h) => selectedBaseRoms.includes(h.baseRomId)); + out = out.filter((h) => h.baseRomId && selectedBaseRoms.includes(h.baseRomId)); } // Filter to hacks whose base ROM is ready (linked with permission or cached) if (onlyReady) { - out = out.filter((h) => readyBaseRomIds.has(h.baseRomId)); + out = out.filter((h) => !h.isArchive && h.baseRomId && readyBaseRomIds.has(h.baseRomId)); } return out; }, [hacks, query, selectedTags, selectedBaseRoms, onlyReady, readyBaseRomIds]); diff --git a/src/components/HackCard.tsx b/src/components/HackCard.tsx index b83e86f..3a9e0c3 100644 --- a/src/components/HackCard.tsx +++ b/src/components/HackCard.tsx @@ -11,8 +11,9 @@ import useEmblaCarousel from "embla-carousel-react"; import { usePathname } from "next/navigation"; import { FaRegImages } from "react-icons/fa6"; import { ImDownload } from "react-icons/im"; +import { FaArchive } from "react-icons/fa"; -type CardHack = { +export interface HackCardAttributes { slug: string; title: string; author: string; @@ -23,15 +24,19 @@ type CardHack = { version: string; summary?: string; description?: string; + isArchive?: boolean; }; -export default function HackCard({ hack, clickable = true, className = "" }: { hack: CardHack; clickable?: boolean; className?: string }) { +export default function HackCard({ hack, clickable = true, className = "" }: { hack: HackCardAttributes; clickable?: boolean; className?: string }) { + const isArchive = !!hack.isArchive; const { isLinked, hasPermission, hasCached } = useBaseRoms(); const match = baseRoms.find((r) => r.id === hack.baseRomId); const baseId = match?.id ?? undefined; const baseName = match?.name ?? undefined; - const linked = baseId ? isLinked(baseId) : false; - const ready = baseId ? hasPermission(baseId) || hasCached(baseId) : false; + + // Only compute base ROM readiness for non-archive hacks + const linked = !isArchive && baseId ? isLinked(baseId) : false; + const ready = !isArchive && baseId ? hasPermission(baseId) || hasCached(baseId) : false; const images = (hack.covers && hack.covers.length > 0 ? hack.covers : []).filter(Boolean); const isCarousel = images.length > 1; const pathname = usePathname(); @@ -121,7 +126,7 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h ) : null}
- {hack.tags.slice(0, 2).map((t) => ( + {hack.tags.slice(0, isArchive ? 3 : 2).map((t) => ( ))} - - {ready ? "Ready" : linked ? "Permission needed" : "Base ROM needed"} - + {!isArchive && ( + + {ready ? "Ready" : linked ? "Permission needed" : "Base ROM needed"} + + )}
+ {isArchive && ( +
+ +
+ )} {isCarousel && (
{images.map((_, i) => ( @@ -164,8 +176,8 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h
-
-
+
+

{hack.title}

@@ -175,10 +187,12 @@ export default function HackCard({ hack, clickable = true, className = "" }: { h

By {hack.author}

-
+ {!isArchive && ( +
- {formatCompactNumber(hack.downloads)} -
+ {formatCompactNumber(hack.downloads)} +
+ )}

{(() => {