From 79b4f0580902214699250cb44f2634940b214178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:36:17 -0600 Subject: [PATCH] feat: start of popup --- packages/i18n/strings/en.json | 3 +- .../{index.css => index.module.css} | 41 +++++++++- .../src/components/AvatarCard/index.tsx | 72 +++++++++++++++-- .../src/components/AvatarsList/index.css | 6 +- .../src/components/AvatarsList/index.tsx | 8 +- .../website-ue/src/components/Badge/index.css | 1 - .../src/components/Dialog/index.module.css | 33 ++++++++ .../src/components/Dialog/index.tsx | 80 +++++++++++++++++++ 8 files changed, 225 insertions(+), 19 deletions(-) rename packages/website-ue/src/components/AvatarCard/{index.css => index.module.css} (57%) create mode 100644 packages/website-ue/src/components/Dialog/index.module.css create mode 100644 packages/website-ue/src/components/Dialog/index.tsx diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index b6603b340..81dc2d47e 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -49,6 +49,7 @@ }, "avatars": { - "search": "Search avatars..." + "search": "Search avatars...", + "no_description": "Avatar has no description." } } diff --git a/packages/website-ue/src/components/AvatarCard/index.css b/packages/website-ue/src/components/AvatarCard/index.module.css similarity index 57% rename from packages/website-ue/src/components/AvatarCard/index.css rename to packages/website-ue/src/components/AvatarCard/index.module.css index e2fc6e3f7..298b15503 100644 --- a/packages/website-ue/src/components/AvatarCard/index.css +++ b/packages/website-ue/src/components/AvatarCard/index.module.css @@ -1,15 +1,15 @@ -.avatar-card { +.card { display: flex; flex-direction: column; align-items: center; gap: var(--space-4); - .avatar-series { + .series { position: relative; width: 100%; aspect-ratio: 7 / 8; - .avatar-entry { + .entry { position: absolute; border-radius: var(--space-3); @@ -30,10 +30,12 @@ position: absolute; bottom: var(--space-3); right: var(--space-3); + + box-shadow: var(--shadow-aggressive); } } - .avatar-label { + .label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; @@ -45,3 +47,34 @@ } } } + +.details { + display: flex; + gap: var(--space-4); + align-items: center; + + .preview { + display: flex; + flex-direction: column; + align-items: center; + + .image { + aspect-ratio: 7 / 8; + width: 8rem; + + background-size: cover; + background-position: center; + } + + .grade { + margin-top: calc(var(--space-3) * -1); + } + } + + .description { + display: flex; + flex-direction: column; + + max-width: 20rem; + } +} diff --git a/packages/website-ue/src/components/AvatarCard/index.tsx b/packages/website-ue/src/components/AvatarCard/index.tsx index b6bf3c824..73c64ac2c 100644 --- a/packages/website-ue/src/components/AvatarCard/index.tsx +++ b/packages/website-ue/src/components/AvatarCard/index.tsx @@ -1,12 +1,15 @@ import { literals } from "@blitzkit/i18n"; +import { Grade } from "@protos/blitz_static_standard_grades_enum"; +import { useGameStrings } from "../../hooks/useGameStrings"; import { useStrings } from "../../hooks/useStrings"; import type { Avatar } from "../../protos/avatar"; import type { PropsWithSkeleton } from "../../types/propsWithSkeleton"; import { Badge } from "../Badge"; +import { Dialog } from "../Dialog"; import { InlineSkeleton } from "../InlineSkeleton"; import { Skeleton } from "../Skeleton"; import { Text } from "../Text"; -import "./index.css"; +import styles from "./index.module.css"; interface Props { name: string; @@ -17,15 +20,68 @@ const MAX_CARDS = 3; const CARD_TRIM = "var(--space-3)"; export function AvatarCard(props: PropsWithSkeleton) { - return ( -
+ const gameStrings = useGameStrings("ProfileAvatarEntity"); + const strings = useStrings(); + + const card = ( +
- + {props.skeleton ? : props.name}
); + + if (props.skeleton) return card; + + return ( + + {card} + + + {props.name} + + + {[...props.avatars].reverse().map((avatar) => { + const description = gameStrings[avatar.stuff_ui!.description]; + const obtaining = gameStrings[avatar.stuff_ui!.obtaining_methods]; + const hasDescription = description || obtaining; + + return ( +
+
+
+ + {Grade[avatar.stuff_ui!.grade]} + +
+ +
+ + {hasDescription ? ( + <> + {description} {obtaining} + + ) : ( + strings.avatars.no_description + )} + +
+
+ ); + })} + + + ); } interface SeriesProps { @@ -37,12 +93,12 @@ function Series(props: PropsWithSkeleton) { const strings = useStrings(); return ( -
+
{!props.skeleton && props.avatars.slice(-MAX_CARDS).map((avatar, index) => (
) { ))} {!props.skeleton && props.avatars.length > 1 && ( - + {literals(strings.units.x, { value: props.avatars.length })} )} - {props.skeleton && } + {props.skeleton && }
); } diff --git a/packages/website-ue/src/components/AvatarsList/index.css b/packages/website-ue/src/components/AvatarsList/index.css index 29b34f401..dac2ba4cb 100644 --- a/packages/website-ue/src/components/AvatarsList/index.css +++ b/packages/website-ue/src/components/AvatarsList/index.css @@ -1,5 +1,9 @@ .avatars-list { display: grid; - grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr)); gap: var(--space-5) var(--space-4); + grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr)); + + @media (min-width: 32rem) { + grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); + } } diff --git a/packages/website-ue/src/components/AvatarsList/index.tsx b/packages/website-ue/src/components/AvatarsList/index.tsx index 77a332faf..1197abaae 100644 --- a/packages/website-ue/src/components/AvatarsList/index.tsx +++ b/packages/website-ue/src/components/AvatarsList/index.tsx @@ -18,13 +18,13 @@ export function AvatarsList() { const locale = useLocale(); const search = Avatars.use((state) => state.search); const { avatars } = useAwait(() => api.avatars(), "avatars"); - const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity"); + const gameStrings = useGameStrings("ProfileAvatarEntity"); const groups = useMemo(() => { const groups = new Map(); for (const avatar of avatars) { const name = - profileAvatarEntityStrings[avatar.stuff_ui!.display_name] ?? + gameStrings[avatar.stuff_ui!.display_name] ?? avatar.stuff_ui!.display_name; if (groups.has(name)) { @@ -75,8 +75,8 @@ export function AvatarsList() { return (
diff --git a/packages/website-ue/src/components/Badge/index.css b/packages/website-ue/src/components/Badge/index.css index dbecd8f99..dc1f8479d 100644 --- a/packages/website-ue/src/components/Badge/index.css +++ b/packages/website-ue/src/components/Badge/index.css @@ -1,6 +1,5 @@ .badge { border-radius: var(--max-size); - box-shadow: var(--shadow-aggressive); padding: var(--space-2) var(--space-3); diff --git a/packages/website-ue/src/components/Dialog/index.module.css b/packages/website-ue/src/components/Dialog/index.module.css new file mode 100644 index 000000000..7e9c05624 --- /dev/null +++ b/packages/website-ue/src/components/Dialog/index.module.css @@ -0,0 +1,33 @@ +.trigger { + display: contents; + cursor: pointer; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100dvh; + + background-color: var(--black-a9); + z-index: 1; + + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-4); + + .content { + display: flex; + flex-direction: column; + gap: var(--space-4); + + min-width: 25rem; + + background-color: var(--gray-3); + padding: var(--space-4); + box-shadow: var(--shadow-interactive), var(--shadow-aggressive); + border-radius: var(--space-3); + } +} diff --git a/packages/website-ue/src/components/Dialog/index.tsx b/packages/website-ue/src/components/Dialog/index.tsx new file mode 100644 index 000000000..b28a95e37 --- /dev/null +++ b/packages/website-ue/src/components/Dialog/index.tsx @@ -0,0 +1,80 @@ +import { createContext, useContext, useState, type ReactNode } from "react"; +import styles from "./index.module.css"; + +const DialogContext = createContext<{ + open: boolean; + setOpen: (open: boolean) => void; +} | null>(null); + +function useDialog() { + const context = useContext(DialogContext); + + if (context === null) { + throw new Error("Dialog components must be used within a Dialog.Root"); + } + + return context; +} + +interface RootProps { + children: ReactNode; +} + +function Root({ children }: RootProps) { + const [open, setOpen] = useState(false); + + return ( + + {children} + + ); +} + +interface TriggerProps { + children: ReactNode; +} + +function Trigger({ children }: TriggerProps) { + const { setOpen } = useDialog(); + + return ( +
{ + setOpen(true); + }} + > + {children} +
+ ); +} + +interface ContentProps { + children: ReactNode; +} + +function Content({ children }: ContentProps) { + const { open, setOpen } = useDialog(); + + if (!open) return null; + + return ( +
{ + setOpen(false); + }} + > +
{ + event.stopPropagation(); + }} + > + {children} +
+
+ ); +} + +export const Dialog = { Root, Trigger, Content };