FEAT: improve server sort and filter ui

This commit is contained in:
John Wesley 2024-07-02 20:10:42 -04:00
parent ba3c15386e
commit 73812c0c66
4 changed files with 245 additions and 17 deletions

View file

@ -0,0 +1,38 @@
import { splitProps, ValidComponent } from "solid-js"
import * as CheckboxPrimitive from "@kobalte/core/checkbox"
import { PolymorphicProps } from "@kobalte/core/polymorphic"
import { cn } from "~/lib/utils"
type CheckboxRootProps<T extends ValidComponent = "div"> =
CheckboxPrimitive.CheckboxRootProps<T> & { class?: string | undefined }
const Checkbox = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, CheckboxRootProps<T>>
) => {
const [local, others] = splitProps(props as CheckboxRootProps, ["class"])
return (
<CheckboxPrimitive.Root class={cn("items-top group flex space-x-2", local.class)} {...others}>
<CheckboxPrimitive.Input class="peer" />
<CheckboxPrimitive.Control class="size-4 shrink-0 rounded-sm border border-primary ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 data-[checked]:border-none data-[checked]:bg-primary data-[checked]:text-primary-foreground">
<CheckboxPrimitive.Indicator>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M5 12l5 5l10 -10" />
</svg>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Control>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View file

@ -0,0 +1,19 @@
import type { Component, ComponentProps } from "solid-js"
import { splitProps } from "solid-js"
import { cn } from "~/lib/utils"
const Label: Component<ComponentProps<"label">> = (props) => {
const [local, others] = splitProps(props, ["class"])
return (
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
local.class
)}
{...others}
/>
)
}
export { Label }

View file

@ -0,0 +1,109 @@
import type { JSX, ValidComponent } from "solid-js"
import { splitProps } from "solid-js"
import { PolymorphicProps } from "@kobalte/core/polymorphic"
import * as SelectPrimitive from "@kobalte/core/select"
import { cn } from "~/lib/utils"
const Select = SelectPrimitive.Root
const SelectValue = SelectPrimitive.Value
const SelectHiddenSelect = SelectPrimitive.HiddenSelect
type SelectTriggerProps<T extends ValidComponent = "button"> =
SelectPrimitive.SelectTriggerProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const SelectTrigger = <T extends ValidComponent = "button">(
props: PolymorphicProps<T, SelectTriggerProps<T>>
) => {
const [local, others] = splitProps(props as SelectTriggerProps, ["class", "children"])
return (
<SelectPrimitive.Trigger
class={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
local.class
)}
{...others}
>
{local.children}
<SelectPrimitive.Icon
as="svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4 opacity-50"
>
<path d="M8 9l4 -4l4 4" />
<path d="M16 15l-4 4l-4 -4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
type SelectContentProps<T extends ValidComponent = "div"> =
SelectPrimitive.SelectContentProps<T> & { class?: string | undefined }
const SelectContent = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, SelectContentProps<T>>
) => {
const [local, others] = splitProps(props as SelectContentProps, ["class"])
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
class={cn(
"relative z-50 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
local.class
)}
{...others}
>
<SelectPrimitive.Listbox class="m-0 p-1" />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
type SelectItemProps<T extends ValidComponent = "li"> = SelectPrimitive.SelectItemProps<T> & {
class?: string | undefined
children?: JSX.Element
}
const SelectItem = <T extends ValidComponent = "li">(
props: PolymorphicProps<T, SelectItemProps<T>>
) => {
const [local, others] = splitProps(props as SelectItemProps, ["class", "children"])
return (
<SelectPrimitive.Item
class={cn(
"relative mt-0 flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class
)}
{...others}
>
<SelectPrimitive.ItemIndicator class="absolute right-2 flex size-3.5 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 12l5 5l10 -10" />
</svg>
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>
</SelectPrimitive.Item>
)
}
export { Select, SelectValue, SelectHiddenSelect, SelectTrigger, SelectContent, SelectItem }

View file

@ -23,6 +23,15 @@ import MaterialSymbolsPerson from '~icons/material-symbols/person';
import MaterialSymbolsPersonCheck from '~icons/material-symbols/person-check'; import MaterialSymbolsPersonCheck from '~icons/material-symbols/person-check';
import MaterialSymbolsNews from '~icons/material-symbols/news'; import MaterialSymbolsNews from '~icons/material-symbols/news';
import MaterialSymbolsComment from '~icons/material-symbols/comment'; import MaterialSymbolsComment from '~icons/material-symbols/comment';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '~/components/ui/select';
import { Checkbox } from '~/components/ui/checkbox';
import { Label } from '~/components/ui/label';
const servers = serversJson as Server[]; const servers = serversJson as Server[];
@ -65,13 +74,34 @@ const languageNames = new Intl.DisplayNames(['en'], {
export default function ServersPage() { export default function ServersPage() {
const [filterRegistration, setFilterRegistration] = createSignal(true); const [filterRegistration, setFilterRegistration] = createSignal(true);
const [langFilter, setLangFilter] = createSignal<string>(''); const [langFilter, setLangFilter] = createSignal<string>('');
const [sort, setSort] = createSignal<'random' | 'activeUsers' | 'totalUsers'>(
'activeUsers',
);
const sortNameMap = {
random: 'Random',
activeUsers: 'Active Users',
totalUsers: 'Total Users',
};
const resultServers = () => { const resultServers = () => {
return servers.filter( return servers
(server) => .filter(
(!filterRegistration() || server.openRegistrations) && (server) =>
(!langFilter() || server.language == langFilter()), (!filterRegistration() || server.openRegistrations) &&
); (!langFilter() || server.language == langFilter()),
)
.sort((a, b) => {
switch (sort()) {
case 'random':
return 0.5 - Math.random();
case 'activeUsers':
return b.activeMonthUsers - a.activeMonthUsers;
case 'totalUsers':
return b.totalUsers - a.totalUsers;
default:
return 0;
}
});
}; };
return ( return (
@ -79,22 +109,54 @@ export default function ServersPage() {
<h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16"> <h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16">
Mbin Servers Mbin Servers
</h1> </h1>
<label> <div class="flex">
Open Registration Only:{' '} <Checkbox
<input id="open-registration"
type="checkbox" class="mr-2"
checked={filterRegistration()} checked={filterRegistration()}
onChange={(e) => setFilterRegistration(e.target.checked)} onChange={(v) => setFilterRegistration(v)}
/> />
</label> <Label for="open-registration-input">Open Registration Only</Label>
</div>
<br /> <br />
Language: Language:
<select onChange={(e) => setLangFilter(e.target.value)}> <Select
<option value="">All Languages</option> value={langFilter()}
<For each={[...new Set(servers.map((v) => v.language))]}> onChange={setLangFilter}
{(lang) => <option value={lang}>{languageNames.of(lang)}</option>} options={[...new Set(servers.map((v) => v.language))]}
</For> placeholder="All Languages"
</select> itemComponent={(props) => (
<SelectItem item={props.item}>
{languageNames.of(props.item.rawValue)}
</SelectItem>
)}
>
<SelectTrigger aria-label="Fruit" class="w-[180px]">
<SelectValue<string>>
{(state) => languageNames.of(state.selectedOption())}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<br />
Sort by:
<Select
value={sort()}
onChange={setSort}
options={Object.keys(sortNameMap)}
itemComponent={(props) => (
<SelectItem item={props.item}>
{sortNameMap[props.item.rawValue]}
</SelectItem>
)}
>
<SelectTrigger aria-label="Fruit" class="w-[180px]">
<SelectValue<string>>
{(state) => sortNameMap[state.selectedOption()]}
</SelectValue>
</SelectTrigger>
<SelectContent />
</Select>
<div class="my-4 flex flex-wrap gap-3 justify-center text-xl"> <div class="my-4 flex flex-wrap gap-3 justify-center text-xl">
<Chip>{servers.length} servers</Chip> <Chip>{servers.length} servers</Chip>
<Chip> <Chip>