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} +
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 )}
- - setSortBy(e.target.value as any)} - className="w-full md:w-auto rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" - > - - - - + onChange={(value) => setSortBy(value as any)} + className="w-full md:w-auto" + options={[ + { value: "created_at", label: "Sort by date" }, + { value: "title", label: "Sort by title" }, + { value: "original_author", label: "Sort by author" }, + ]} + />
-
+
{sortIcon} - +
+ 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 => ( - - ))} - + { 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 ? ( - + 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 ? ( - + 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 && ( + + + )} + + )} + + ); + })} + + +
+ )} +
+ ); +}