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 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>
|
||||||
|
|
Loading…
Add table
Reference in a new issue