mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-06-21 06:39:48 -05:00
alternative-dropdowns: swapped out the native html select component for a new primitive
This commit is contained in:
parent
a013565c6f
commit
39e840beb6
|
|
@ -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() {
|
|||
)}
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="topic" className="text-sm text-foreground/80">Topic</label>
|
||||
<select
|
||||
<Select
|
||||
id="topic"
|
||||
name="topic"
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value as Topic)}
|
||||
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)]"
|
||||
>
|
||||
{(Object.keys(topicLabels) as Topic[]).map((key) => (
|
||||
<option key={key} value={key}>{topicLabels[key]}</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(value) => setTopic(value as Topic)}
|
||||
options={(Object.keys(topicLabels) as Topic[]).map((key) => ({
|
||||
value: key,
|
||||
label: topicLabels[key],
|
||||
}))}
|
||||
/>
|
||||
<span className="text-xs text-foreground/60">
|
||||
Choose the most relevant topic so we can help you better.
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||
<select
|
||||
<Select
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value as "all" | "downloadable" | "informational")}
|
||||
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)]"
|
||||
>
|
||||
<option value="all">All Archives</option>
|
||||
<option value="downloadable">Downloadable</option>
|
||||
<option value="informational">Informational</option>
|
||||
</select>
|
||||
<select
|
||||
onChange={(value) => setFilter(value as "all" | "downloadable" | "informational")}
|
||||
className="w-full md:w-auto"
|
||||
options={[
|
||||
{ value: "all", label: "All Archives" },
|
||||
{ value: "downloadable", label: "Downloadable" },
|
||||
{ value: "informational", label: "Informational" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={sortBy}
|
||||
onChange={(e) => 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)]"
|
||||
>
|
||||
<option value="created_at">Sort by date</option>
|
||||
<option value="title">Sort by title</option>
|
||||
<option value="original_author">Sort by author</option>
|
||||
</select>
|
||||
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" },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<select value={language} onChange={(e) => 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 => (
|
||||
<option key={l} value={l}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
value={language}
|
||||
onChange={setLanguage}
|
||||
className={languageChanged ? 'ring-[var(--ring)]' : ''}
|
||||
options={['English','Spanish','French','German','Italian','Portuguese','Japanese','Chinese','Korean','Other'].map(l => ({
|
||||
value: l,
|
||||
label: l,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Current version</label>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Platform <span className="text-red-500">*</span></label>
|
||||
{!isDummy ? (
|
||||
<select
|
||||
<Select
|
||||
value={platform}
|
||||
onChange={(e) => { 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"
|
||||
>
|
||||
<option value="" disabled>Select platform</option>
|
||||
{(["GB","GBC","GBA","NDS"] as const).map(p => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</select>
|
||||
placeholder="Select platform"
|
||||
options={(["GB","GBC","GBA","NDS"] as const).map(p => ({
|
||||
value: p,
|
||||
label: p,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">{platform || ""}</div>
|
||||
)}
|
||||
|
|
@ -755,19 +755,16 @@ export default function HackSubmitForm({
|
|||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Base ROM <span className="text-red-500">*</span></label>
|
||||
{!isDummy ? (
|
||||
<select
|
||||
<Select
|
||||
value={baseRom}
|
||||
onChange={(e) => 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"
|
||||
>
|
||||
<option value="" disabled>{platform ? "Select base rom" : "Select platform first"}</option>
|
||||
{baseRoms.filter(r => !platform || r.platform === platform).map(({ id, name, region }) => (
|
||||
<option key={id} value={id}>
|
||||
{name.replace('Pokémon ', '')} ({region})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
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})`,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">{baseRoms.find(r=>r.id===baseRom)?.name || baseRom}</div>
|
||||
)}
|
||||
|
|
@ -776,16 +773,15 @@ export default function HackSubmitForm({
|
|||
<div className="grid gap-2">
|
||||
<label className="text-sm text-foreground/80">Language <span className="text-red-500">*</span></label>
|
||||
{!isDummy ? (
|
||||
<select
|
||||
<Select
|
||||
value={language}
|
||||
onChange={(e) => 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)]"
|
||||
>
|
||||
<option value="" disabled>Select language</option>
|
||||
{['English','Spanish','French','German','Italian','Portuguese','Japanese','Chinese','Korean','Other'].map(l => (
|
||||
<option key={l} value={l}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={setLanguage}
|
||||
placeholder="Select language"
|
||||
options={['English','Spanish','French','German','Italian','Portuguese','Japanese','Chinese','Korean','Other'].map(l => ({
|
||||
value: l,
|
||||
label: l,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<div role="textbox" aria-disabled className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">{language}</div>
|
||||
)}
|
||||
|
|
|
|||
90
src/components/Primitives/Select.tsx
Normal file
90
src/components/Primitives/Select.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"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;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function Select({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
placeholder = "Select an option",
|
||||
disabled = false,
|
||||
className = "",
|
||||
id,
|
||||
name,
|
||||
}: SelectProps) {
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
|
||||
return (
|
||||
<Listbox value={value} onChange={onChange} disabled={disabled}>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<ListboxButton
|
||||
id={id}
|
||||
className={`relative h-11 w-full rounded-md bg-[var(--surface-2)] px-3 pr-10 text-left text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)] disabled:opacity-50 ${className}`}
|
||||
>
|
||||
<span className={`block truncate ${selectedOption ? "" : "text-foreground/60"}`}>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<FiChevronDown className={`h-4 w-4 text-foreground/60 transition-transform ${open ? "rotate-180" : ""}`} aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
{name && <input type="hidden" name={name} value={value} />}
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<ListboxOptions className="absolute z-10 mt-1 max-h-60 min-w-full max-w-[400px] w-max overflow-auto rounded-md bg-background/95 backdrop-blur-sm py-1 text-sm shadow-lg ring-1 ring-[var(--border)] focus:outline-none">
|
||||
{options.map((option) => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
className={({ focus }) =>
|
||||
`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 }) => (
|
||||
<>
|
||||
<span className={`block truncate ${selected ? "font-medium" : "font-normal"}`}>
|
||||
{option.label}
|
||||
</span>
|
||||
{selected && (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-foreground">
|
||||
<FiCheck className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user