From 79f4b86152921b8e8506161199269c8710a38f87 Mon Sep 17 00:00:00 2001
From: jtb9 <31317998+jtb9@users.noreply.github.com>
Date: Sun, 21 Dec 2025 18:52:47 -0500
Subject: [PATCH 1/2] Alternative dropdowns (#25)
---
src/components/Contact/ContactForm.tsx | 16 ++--
src/components/Dashboard/ArchivesList.tsx | 35 ++++----
src/components/Discover/DiscoverBrowser.tsx | 51 ++++++-----
src/components/Hack/HackEditForm.tsx | 15 ++--
src/components/Hack/HackSubmitForm.tsx | 54 ++++++-----
src/components/Primitives/Select.tsx | 99 +++++++++++++++++++++
6 files changed, 189 insertions(+), 81 deletions(-)
create mode 100644 src/components/Primitives/Select.tsx
diff --git a/src/components/Contact/ContactForm.tsx b/src/components/Contact/ContactForm.tsx
index 58f664c..6f99903 100644
--- a/src/components/Contact/ContactForm.tsx
+++ b/src/components/Contact/ContactForm.tsx
@@ -4,6 +4,7 @@ import React, { useActionState } from "react";
import { useSearchParams } from "next/navigation";
import { validateEmail } from "@/utils/auth";
import { sendContact, type ContactActionState } from "@/app/contact/actions";
+import Select from "@/components/Primitives/Select";
type Topic =
| "general"
@@ -70,17 +71,16 @@ export default function ContactForm() {
)}
-
+ onChange={(value) => setTopic(value as Topic)}
+ options={(Object.keys(topicLabels) as Topic[]).map((key) => ({
+ value: key,
+ label: topicLabels[key],
+ }))}
+ />
Choose the most relevant topic so we can help you better.
diff --git a/src/components/Dashboard/ArchivesList.tsx b/src/components/Dashboard/ArchivesList.tsx
index 199961d..dde0afc 100644
--- a/src/components/Dashboard/ArchivesList.tsx
+++ b/src/components/Dashboard/ArchivesList.tsx
@@ -5,6 +5,7 @@ import Link from "next/link";
import { FiExternalLink, FiEdit2, FiTrash2, FiChevronLeft, FiChevronRight, FiArrowDown, FiSearch, FiLoader, FiDownload, FiInfo, FiBarChart2 } from "react-icons/fi";
import { getArchives, deleteArchive } from "@/app/dashboard/archives/actions";
import { baseRoms } from "@/data/baseRoms";
+import Select from "@/components/Primitives/Select";
type Archive = {
slug: string;
@@ -118,24 +119,26 @@ export default function ArchivesList({ initialData, isAdmin = false }: { initial
)}
-
-
-
+
{sortIcon}
-
{
- const nextSort = e.target.value as DiscoverSortOption;
- setSort(nextSort);
- // Keep URL query param in sync so refresh/back preserves sort
- const current = searchParams ? new URLSearchParams(searchParams.toString()) : new URLSearchParams();
- current.set("sort", nextSort);
- const queryString = current.toString();
- const url = queryString ? `${pathname}?${queryString}` : pathname;
- router.replace(url);
- }}
- className="w-full h-full bg-transparent pl-1 pr-0 text-sm text-foreground focus:outline-none focus:ring-0"
- >
-
-
-
-
-
-
+
+ {
+ const nextSort = value as DiscoverSortOption;
+ setSort(nextSort);
+ const current = searchParams ? new URLSearchParams(searchParams.toString()) : new URLSearchParams();
+ current.set("sort", nextSort);
+ const queryString = current.toString();
+ const url = queryString ? `${pathname}?${queryString}` : pathname;
+ router.replace(url);
+ }}
+ options={SORT_OPTIONS}
+ // this css gets a little janky, but it gets the job done
+ className="flex h-11 w-full items-center rounded-none bg-transparent px-0 pl-1 pr-8 text-left sm:w-fit !ring-0 focus:ring-0"
+ dropdownClassName="-left-[2.313rem] top-[44px] !min-w-0 !max-w-none !w-[calc(100%_+_3.125rem)] sm:left-auto sm:right-[-12px] sm:!w-max"
+ />
+
diff --git a/src/components/Hack/HackEditForm.tsx b/src/components/Hack/HackEditForm.tsx
index e3dc085..e71b8e9 100644
--- a/src/components/Hack/HackEditForm.tsx
+++ b/src/components/Hack/HackEditForm.tsx
@@ -10,6 +10,7 @@ import Image from "next/image";
import { createClient } from "@/utils/supabase/client";
import { updateHack, saveHackCovers, presignCoverUpload } from "@/app/hack/actions";
import SortableCovers from "@/components/Hack/SortableCovers";
+import Select from "@/components/Primitives/Select";
interface HackEditFormProps {
slug: string;
@@ -393,11 +394,15 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) {
)}
- setLanguage(e.target.value)} className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 ${languageChanged ? 'ring-[var(--ring)] bg-[var(--surface-2)]' : 'bg-[var(--surface-2)] ring-[var(--border)]'}`}>
- {['English','Spanish','French','German','Italian','Portuguese','Japanese','Chinese','Korean','Other'].map(l => (
-
- ))}
-
+ ({
+ value: l,
+ label: l,
+ }))}
+ />
diff --git a/src/components/Hack/HackSubmitForm.tsx b/src/components/Hack/HackSubmitForm.tsx
index 762a34a..2f40781 100644
--- a/src/components/Hack/HackSubmitForm.tsx
+++ b/src/components/Hack/HackSubmitForm.tsx
@@ -18,6 +18,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
import { useBaseRoms } from "@/contexts/BaseRomContext";
import TagSelector from "@/components/Submit/TagSelector";
import BinFile from "rom-patcher-js/rom-patcher-js/modules/BinFile.js";
+import Select from "@/components/Primitives/Select";
import BPS from "rom-patcher-js/rom-patcher-js/modules/RomPatcher.format.bps.js";
import { sha1Hex } from "@/utils/hash";
import { platformAccept, setDraftCovers, getDraftCovers, deleteDraftCovers } from "@/utils/idb";
@@ -733,17 +734,16 @@ export default function HackSubmitForm({
{!isDummy ? (
-
{ if ((newCoverFiles.length) > 0) return; setPlatform(e.target.value as any); setBaseRom(""); }}
+ onChange={(value) => { if ((newCoverFiles.length) > 0) return; setPlatform(value as any); setBaseRom(""); }}
disabled={newCoverFiles.length > 0}
- className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-50"
- >
-
- {(["GB","GBC","GBA","NDS"] as const).map(p => (
-
- ))}
-
+ placeholder="Select platform"
+ options={(["GB","GBC","GBA","NDS"] as const).map(p => ({
+ value: p,
+ label: p,
+ }))}
+ />
) : (
{platform || ""}
)}
@@ -755,19 +755,16 @@ export default function HackSubmitForm({
{!isDummy ? (
-
setBaseRom(e.target.value)}
+ onChange={setBaseRom}
disabled={!platform}
- className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-50"
- >
-
- {baseRoms.filter(r => !platform || r.platform === platform).map(({ id, name, region }) => (
-
- ))}
-
+ placeholder={platform ? "Select base rom" : "Select platform first"}
+ options={baseRoms.filter(r => !platform || r.platform === platform).map(({ id, name, region }) => ({
+ value: id,
+ label: `${name.replace('Pokémon ', '')} (${region})`,
+ }))}
+ />
) : (
{baseRoms.find(r=>r.id===baseRom)?.name || baseRom}
)}
@@ -776,16 +773,15 @@ export default function HackSubmitForm({
{!isDummy ? (
-
setLanguage(e.target.value)}
- className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
- >
-
- {['English','Spanish','French','German','Italian','Portuguese','Japanese','Chinese','Korean','Other'].map(l => (
-
- ))}
-
+ onChange={setLanguage}
+ placeholder="Select language"
+ options={['English','Spanish','French','German','Italian','Portuguese','Japanese','Chinese','Korean','Other'].map(l => ({
+ value: l,
+ label: l,
+ }))}
+ />
) : (
{language}
)}
diff --git a/src/components/Primitives/Select.tsx b/src/components/Primitives/Select.tsx
new file mode 100644
index 0000000..99f923e
--- /dev/null
+++ b/src/components/Primitives/Select.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import React, { Fragment } from "react";
+import { Listbox, ListboxButton, ListboxOptions, ListboxOption, Transition } from "@headlessui/react";
+import { FiChevronDown, FiCheck } from "react-icons/fi";
+
+export interface SelectOption {
+ value: string;
+ label: string;
+ disabled?: boolean;
+ icon?: React.ComponentType<{ className?: string }>;
+}
+
+interface SelectProps {
+ value: string;
+ onChange: (value: string) => void;
+ options: SelectOption[];
+ placeholder?: string;
+ disabled?: boolean;
+ className?: string;
+ dropdownClassName?: string;
+ dropdownAlign?: "left" | "right";
+ id?: string;
+ name?: string;
+}
+
+export default function Select({
+ value,
+ onChange,
+ options,
+ placeholder = "Select an option",
+ disabled = false,
+ className = "",
+ dropdownClassName = "",
+ dropdownAlign = "left",
+ id,
+ name,
+}: SelectProps) {
+ const selectedOption = options.find((opt) => opt.value === value);
+
+ return (
+
+ {({ open }) => (
+
+
+
+ {selectedOption?.label || placeholder}
+
+
+
+
+
+ {name && }
+
+
+ {options.map((option) => {
+ const Icon = option.icon;
+ return (
+
+ `relative cursor-pointer select-none py-2 pl-10 pr-4 ${
+ focus ? "bg-black/5 dark:bg-white/10" : ""
+ } ${option.disabled ? "opacity-50 cursor-not-allowed" : ""}`
+ }
+ >
+ {({ selected }) => (
+ <>
+
+ {Icon && }
+ {option.label}
+
+ {selected && (
+
+
+
+ )}
+ >
+ )}
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}
From 26b40ca4570f08212d70d81e3e4b34ffe3b2287b Mon Sep 17 00:00:00 2001
From: jschoeny
Date: Mon, 22 Dec 2025 10:09:42 -1000
Subject: [PATCH 2/2] Add more info to pending hacks admin view
---
src/app/dashboard/page.tsx | 53 +++++++++++++++++++++++++++++++-------
1 file changed, 43 insertions(+), 10 deletions(-)
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 45fe4f1..7a287f8 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,4 +1,4 @@
-import { createClient } from "@/utils/supabase/server";
+import { createClient, createServiceClient } from "@/utils/supabase/server";
import Link from "next/link";
import { redirect } from "next/navigation";
import { FiAlertTriangle, FiExternalLink } from "react-icons/fi";
@@ -14,7 +14,12 @@ export default async function DashboardPage() {
if (!user) redirect("/login");
const { data: isAdmin } = await supa.rpc("is_admin");
- let pendingHacks: (HackRow & { created_by: string; creator_username: string | null; creator_full_name: string | null })[] = [];
+ let pendingHacks: (HackRow & {
+ created_by: string;
+ creator_username: string | null;
+ creator_full_name: string | null;
+ creator_email: string | null
+ })[] = [];
if (isAdmin) {
const { data: pendingHacksData } = await supa
.from("hacks")
@@ -37,10 +42,26 @@ export default async function DashboardPage() {
fullNameById.set(p.id, p.full_name);
});
+ // Fetch creator emails using service client (admin API)
+ const serviceClient = await createServiceClient();
+ const emailById = new Map();
+ for (const userId of creatorIds) {
+ try {
+ const { data: userData, error } = await serviceClient.auth.admin.getUserById(userId);
+ if (!error && userData?.user?.email) {
+ emailById.set(userId, userData.user.email);
+ }
+ } catch (error) {
+ // Silently fail if we can't get the email
+ console.error(`Failed to get email for user ${userId}:`, error);
+ }
+ }
+
pendingHacks = pendingHacksData.map((h) => ({
...h,
creator_username: usernameById.get(h.created_by as string) || null,
creator_full_name: fullNameById.get(h.created_by as string) || null,
+ creator_email: emailById.get(h.created_by as string) || null,
}));
}
}
@@ -89,8 +110,8 @@ export default async function DashboardPage() {
{/* Header row (desktop only) */}
-
Title
-
Creator
+
Title
+
Creator
Created
@@ -119,7 +140,7 @@ export default async function DashboardPage() {
>
{/* Desktop row */}
-
+
@@ -135,9 +156,12 @@ export default async function DashboardPage() {
-
+
{h.creator_full_name &&
{h.creator_full_name}
}
{creator}
+ {h.creator_email && (
+
{h.creator_email}
+ )}
@@ -160,10 +184,19 @@ export default async function DashboardPage() {
)}
/{h.slug}
-
-
{creator}
-
•
-
{createdDate}
+
+ {(h.creator_full_name || h.creator_email) && (
+
+ {h.creator_full_name && {h.creator_full_name}}
+ {h.creator_full_name && h.creator_email && •}
+ {h.creator_email && {h.creator_email}}
+
+ )}
+
+ {creator}
+ •
+ {createdDate}
+