mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-10-02 05:48:22 -05:00
Refactor and implement submit form functionality
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user