mirror of
https://github.com/MbinOrg/mbin-website.git
synced 2025-07-03 00:28:58 +00:00
FEAT: improve server sort and filter ui
This commit is contained in:
parent
ba3c15386e
commit
73812c0c66
4 changed files with 245 additions and 17 deletions
38
src/components/ui/checkbox.tsx
Normal file
38
src/components/ui/checkbox.tsx
Normal 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 }
|
19
src/components/ui/label.tsx
Normal file
19
src/components/ui/label.tsx
Normal 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 }
|
109
src/components/ui/select.tsx
Normal file
109
src/components/ui/select.tsx
Normal 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 }
|
|
@ -23,6 +23,15 @@ import MaterialSymbolsPerson from '~icons/material-symbols/person';
|
|||
import MaterialSymbolsPersonCheck from '~icons/material-symbols/person-check';
|
||||
import MaterialSymbolsNews from '~icons/material-symbols/news';
|
||||
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[];
|
||||
|
||||
|
@ -65,13 +74,34 @@ const languageNames = new Intl.DisplayNames(['en'], {
|
|||
export default function ServersPage() {
|
||||
const [filterRegistration, setFilterRegistration] = createSignal(true);
|
||||
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 = () => {
|
||||
return servers.filter(
|
||||
return servers
|
||||
.filter(
|
||||
(server) =>
|
||||
(!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 (
|
||||
|
@ -79,22 +109,54 @@ export default function ServersPage() {
|
|||
<h1 class="max-6-xs text-6xl text-sky-600 font-extralight uppercase my-16">
|
||||
Mbin Servers
|
||||
</h1>
|
||||
<label>
|
||||
Open Registration Only:{' '}
|
||||
<input
|
||||
type="checkbox"
|
||||
<div class="flex">
|
||||
<Checkbox
|
||||
id="open-registration"
|
||||
class="mr-2"
|
||||
checked={filterRegistration()}
|
||||
onChange={(e) => setFilterRegistration(e.target.checked)}
|
||||
onChange={(v) => setFilterRegistration(v)}
|
||||
/>
|
||||
</label>
|
||||
<Label for="open-registration-input">Open Registration Only</Label>
|
||||
</div>
|
||||
<br />
|
||||
Language:
|
||||
<select onChange={(e) => setLangFilter(e.target.value)}>
|
||||
<option value="">All Languages</option>
|
||||
<For each={[...new Set(servers.map((v) => v.language))]}>
|
||||
{(lang) => <option value={lang}>{languageNames.of(lang)}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<Select
|
||||
value={langFilter()}
|
||||
onChange={setLangFilter}
|
||||
options={[...new Set(servers.map((v) => v.language))]}
|
||||
placeholder="All Languages"
|
||||
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">
|
||||
<Chip>{servers.length} servers</Chip>
|
||||
<Chip>
|
||||
|
|
Loading…
Add table
Reference in a new issue