diff --git a/packages/website-ue/src/components/AvatarControls/index.css b/packages/website-ue/src/components/AvatarControls/index.css index e69de29bb..603a01a9c 100644 --- a/packages/website-ue/src/components/AvatarControls/index.css +++ b/packages/website-ue/src/components/AvatarControls/index.css @@ -0,0 +1,7 @@ +.avatar-controls { + display: flex; + + .search { + flex: 1; + } +} diff --git a/packages/website-ue/src/components/AvatarControls/index.tsx b/packages/website-ue/src/components/AvatarControls/index.tsx index 1b11d810f..812a87946 100644 --- a/packages/website-ue/src/components/AvatarControls/index.tsx +++ b/packages/website-ue/src/components/AvatarControls/index.tsx @@ -1,14 +1,32 @@ import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; +import { useRef } from "react"; import { useStrings } from "../../hooks/useStrings"; +import { Avatars } from "../../stores/avatars"; import { TextField } from "../TextField"; import "./index.css"; export function AvatarControls() { const strings = useStrings(); + const search = useRef(null); return ( -
- +
+ { + const trimmed = event.target.value.trim(); + + Avatars.mutate((draft) => { + if (trimmed.length === 0) { + draft.search = null; + } else { + draft.search = trimmed; + } + }); + }} + >
diff --git a/packages/website-ue/src/components/AvatarsList/index.css b/packages/website-ue/src/components/AvatarsList/index.css new file mode 100644 index 000000000..ec63672bc --- /dev/null +++ b/packages/website-ue/src/components/AvatarsList/index.css @@ -0,0 +1,5 @@ +.avatars-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--space-7), 1fr)); + gap: var(--space-5) var(--space-4); +} diff --git a/packages/website-ue/src/components/AvatarsList/index.tsx b/packages/website-ue/src/components/AvatarsList/index.tsx new file mode 100644 index 000000000..75074338a --- /dev/null +++ b/packages/website-ue/src/components/AvatarsList/index.tsx @@ -0,0 +1,85 @@ +import fuzzysort from "fuzzysort"; +import { useMemo } from "react"; +import { api } from "../../core/api/dynamic"; +import { useAwait } from "../../hooks/useAwait"; +import { useGameStrings } from "../../hooks/useGameStrings"; +import { useLocale } from "../../hooks/useLocale"; +import type { Avatar } from "../../protos/avatar"; +import { Avatars } from "../../stores/avatars"; +import { AvatarCard } from "../AvatarCard"; +import { IncrementalLoader } from "../IncrementalLoader"; +import "./index.css"; + +const { go } = fuzzysort; + +const indexingPattern = /.+_(\d+)$/; + +export function AvatarsList() { + const locale = useLocale(); + const search = Avatars.use((state) => state.search); + const { avatars } = useAwait(() => api.avatars(), "avatars"); + const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity"); + const groups = useMemo(() => { + const groups = new Map(); + + for (const avatar of avatars) { + const name = + profileAvatarEntityStrings[avatar.stuff_ui!.display_name] ?? + avatar.stuff_ui!.display_name; + + if (groups.has(name)) { + groups.get(name)!.push(avatar); + } else { + groups.set(name, [avatar]); + } + } + + return groups; + }, []); + const ordered = useMemo(() => { + const array = Array.from(groups.entries()); + return array + .sort((a, b) => a[0].localeCompare(b[0], locale)) + .map(([name, avatars]) => ({ + key: name, + name, + avatars: avatars.sort((a, b) => { + const gradeA = a.stuff_ui!.grade; + const gradeB = b.stuff_ui!.grade; + let diff = gradeA - gradeB; + + if (diff === 0) { + const matchesA = a.name.match(indexingPattern); + const matchesB = b.name.match(indexingPattern); + + if (matchesA && matchesB) { + const indexA = Number(matchesA[1]); + const indexB = Number(matchesB[1]); + + diff = indexA - indexB; + } + } + + return diff; + }), + })); + }, []); + const filtered = useMemo(() => { + if (search === null) return ordered; + + const searched = go(search, ordered, { keys: ["name"] }); + + return searched.map((result) => result.obj); + }, [search]); + + return ( +
+ +
+ ); +} diff --git a/packages/website-ue/src/components/IncrementalLoader/index.tsx b/packages/website-ue/src/components/IncrementalLoader/index.tsx index 6a203ed97..2a85737bb 100644 --- a/packages/website-ue/src/components/IncrementalLoader/index.tsx +++ b/packages/website-ue/src/components/IncrementalLoader/index.tsx @@ -3,17 +3,17 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import type { SkeletonProps } from "../../types/SkeletonProps"; import "./index.css"; -export interface IncrementalLoaderProps { +export interface IncrementalLoaderProps { initial: number; intermediate: number; rate?: number; skeleton?: boolean; data: Props[]; - Component: (props: SkeletonProps) => ReactNode; + Component: (props: SkeletonProps>) => ReactNode; } -export function IncrementalLoader({ +export function IncrementalLoader({ initial, rate = 2, data, @@ -27,7 +27,7 @@ export function IncrementalLoader({ return ( <> {!skeleton && - sliced.map((item, index) => )} + sliced.map(({ key, ...props }) => )} {times( skeleton diff --git a/packages/website-ue/src/components/TextField/index.css b/packages/website-ue/src/components/TextField/index.css index c6a1a9fe9..65a7890aa 100644 --- a/packages/website-ue/src/components/TextField/index.css +++ b/packages/website-ue/src/components/TextField/index.css @@ -4,7 +4,6 @@ box-sizing: border-box; gap: var(--space-3); - margin: 0 var(--space-4); border-radius: var(--max-size); diff --git a/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx b/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx index 355db2c3c..bcf2a28ac 100644 --- a/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx +++ b/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx @@ -1,13 +1,7 @@ -import { useMemo } from "react"; -import { AvatarCard } from "../../../components/AvatarCard"; import { AvatarControls } from "../../../components/AvatarControls"; -import { IncrementalLoader } from "../../../components/IncrementalLoader"; +import { AvatarsList } from "../../../components/AvatarsList"; import { Section } from "../../../components/Section"; -import { api } from "../../../core/api/dynamic"; -import { useAwait } from "../../../hooks/useAwait"; -import { useGameStrings } from "../../../hooks/useGameStrings"; -import { LocaleProvider, useLocale } from "../../../hooks/useLocale"; -import type { Avatar } from "../../../protos/avatar"; +import { LocaleProvider } from "../../../hooks/useLocale"; interface PageProps extends ContentProps { locale: string; @@ -25,57 +19,7 @@ interface ContentProps { skeleton?: boolean; } -const indexingPattern = /.+_(\d+)$/; - function Content({}: ContentProps) { - const locale = useLocale(); - const { avatars } = useAwait(() => api.avatars(), "avatars"); - const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity"); - const groups = useMemo(() => { - const groups = new Map(); - - for (const avatar of avatars) { - const name = - profileAvatarEntityStrings[avatar.stuff_ui!.display_name] ?? - avatar.stuff_ui!.display_name; - - if (groups.has(name)) { - groups.get(name)!.push(avatar); - } else { - groups.set(name, [avatar]); - } - } - - return groups; - }, []); - const ordered = useMemo(() => { - const array = Array.from(groups.entries()); - return array - .sort((a, b) => a[0].localeCompare(b[0], locale)) - .map(([name, avatars]) => ({ - name, - avatars: avatars.sort((a, b) => { - const gradeA = a.stuff_ui!.grade; - const gradeB = b.stuff_ui!.grade; - let diff = gradeA - gradeB; - - if (diff === 0) { - const matchesA = a.name.match(indexingPattern); - const matchesB = b.name.match(indexingPattern); - - if (matchesA && matchesB) { - const indexA = Number(matchesA[1]); - const indexB = Number(matchesB[1]); - - diff = indexA - indexB; - } - } - - return diff; - }), - })); - }, []); - return ( <>
@@ -83,14 +27,7 @@ function Content({}: ContentProps) {
-
- -
+
); diff --git a/packages/website-ue/src/pages/[...locale]/avatars/index.astro b/packages/website-ue/src/pages/[...locale]/avatars/index.astro index 2286e4766..54cf5399d 100644 --- a/packages/website-ue/src/pages/[...locale]/avatars/index.astro +++ b/packages/website-ue/src/pages/[...locale]/avatars/index.astro @@ -9,14 +9,6 @@ export { getStaticPaths } from "../index.astro"; const { locale } = Astro.props; --- - - diff --git a/packages/website-ue/src/stores/avatars.ts b/packages/website-ue/src/stores/avatars.ts new file mode 100644 index 000000000..94a400ea8 --- /dev/null +++ b/packages/website-ue/src/stores/avatars.ts @@ -0,0 +1,9 @@ +import { Varuna } from "varuna"; + +interface Avatars { + search: string | null; +} + +export const Avatars = new Varuna({ + search: null, +});