138 lines
3.4 KiB
TypeScript
138 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import type { ComponentProps, ReactNode } from "react";
|
|
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { elementIconFilter } from "@/lib/catalog/element";
|
|
|
|
export type AdminSelectOption = {
|
|
value: string;
|
|
label: ReactNode;
|
|
};
|
|
|
|
type AdminDropdownProps = {
|
|
options: readonly AdminSelectOption[];
|
|
value?: string;
|
|
onValueChange: (value: string) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
"aria-label"?: string;
|
|
triggerProps?: Omit<ComponentProps<typeof SelectTrigger>, "children">;
|
|
};
|
|
|
|
export function AdminDropdown({
|
|
options,
|
|
value,
|
|
onValueChange,
|
|
placeholder,
|
|
disabled,
|
|
"aria-label": ariaLabel,
|
|
triggerProps,
|
|
}: AdminDropdownProps) {
|
|
return (
|
|
<Select
|
|
value={value}
|
|
disabled={disabled}
|
|
onValueChange={(next) => {
|
|
if (next !== null) onValueChange(next);
|
|
}}
|
|
>
|
|
<SelectTrigger aria-label={ariaLabel} {...triggerProps}>
|
|
<SelectValue placeholder={placeholder} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
{options.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
type ConstellationSelectProps = Omit<AdminDropdownProps, "options"> & {
|
|
includeNone?: boolean;
|
|
includeRange?: boolean;
|
|
minimum?: 0 | 1;
|
|
};
|
|
|
|
export function ConstellationSelect({
|
|
includeNone = false,
|
|
includeRange = false,
|
|
minimum = 0,
|
|
...props
|
|
}: ConstellationSelectProps) {
|
|
const options: AdminSelectOption[] = [
|
|
...(includeNone ? [{ value: "none", label: "None" }] : []),
|
|
...Array.from({ length: 7 - minimum }, (_, index) => {
|
|
const level = index + minimum;
|
|
return { value: String(level), label: `C${level}` };
|
|
}),
|
|
...(includeRange ? [{ value: "0-6", label: "C0-6" }] : []),
|
|
];
|
|
return <AdminDropdown {...props} options={options} />;
|
|
}
|
|
|
|
type RefinementSelectProps = Omit<AdminDropdownProps, "options"> & {
|
|
includeNone?: boolean;
|
|
includeRange?: boolean;
|
|
};
|
|
|
|
export function RefinementSelect({
|
|
includeNone = false,
|
|
includeRange = false,
|
|
...props
|
|
}: RefinementSelectProps) {
|
|
const options: AdminSelectOption[] = [
|
|
...(includeNone ? [{ value: "none", label: "None" }] : []),
|
|
...Array.from({ length: 5 }, (_, index) => ({
|
|
value: String(index + 1),
|
|
label: `R${index + 1}`,
|
|
})),
|
|
...(includeRange ? [{ value: "1-5", label: "R1-5" }] : []),
|
|
];
|
|
return <AdminDropdown {...props} options={options} />;
|
|
}
|
|
|
|
type ElementSelectProps = Omit<AdminDropdownProps, "options"> & {
|
|
elements: readonly string[];
|
|
iconUrls?: Record<string, string | null | undefined>;
|
|
};
|
|
|
|
export function ElementSelect({ elements, iconUrls = {}, ...props }: ElementSelectProps) {
|
|
return (
|
|
<AdminDropdown
|
|
{...props}
|
|
options={elements.map((element) => ({
|
|
value: element,
|
|
label: (
|
|
<>
|
|
{iconUrls[element] ? (
|
|
<Image
|
|
src={iconUrls[element]}
|
|
alt=""
|
|
width={20}
|
|
height={20}
|
|
className="size-5 object-contain"
|
|
style={{ filter: elementIconFilter(element) }}
|
|
/>
|
|
) : null}
|
|
{element}
|
|
</>
|
|
),
|
|
}))}
|
|
/>
|
|
);
|
|
}
|