From ae5e4f1049b6cde88c8348711b09f0a0bdc03b3f Mon Sep 17 00:00:00 2001 From: Yuzu Date: Mon, 29 Jun 2026 03:22:55 +0700 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20shared=20HeroH?= =?UTF-8?q?eader,=20i18n=20wiring,=20group=20owner=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/features/group/GroupView.tsx | 145 +++++++++--------- .../src/features/profile/ProfileView.tsx | 43 ++---- .../src/features/shared/HeroHeader.tsx | 40 +++++ src/renderer/src/features/world/WorldView.tsx | 93 +++++------ 4 files changed, 181 insertions(+), 140 deletions(-) create mode 100644 src/renderer/src/features/shared/HeroHeader.tsx diff --git a/src/renderer/src/features/group/GroupView.tsx b/src/renderer/src/features/group/GroupView.tsx index 7a20f0e..bd6fde3 100644 --- a/src/renderer/src/features/group/GroupView.tsx +++ b/src/renderer/src/features/group/GroupView.tsx @@ -3,10 +3,13 @@ import type { Group } from "../../../../shared/types/group"; import { Avatar, Banner, Fact, Section, Skeleton, StatTile, Tag } from "../../components/ui"; import { api } from "../../lib/api"; import { useAsync } from "../../lib/useAsync"; -import { compactNumber, formatDate, prettyTag } from "../../lib/format"; +import { compactNumber, formatDate, prettyTag, tagsWithPrefix } from "../../lib/format"; +import { useT } from "../../lib/i18n"; import { useNav } from "../navigation/NavContext"; import { useGroups } from "../../store/groups"; +import { useSocial } from "../../store/social"; import { COL_WIDE } from "../../lib/layout"; +import { HeroHeader } from "../shared/HeroHeader"; import "../profile/profile.css"; export function GroupView({ groupId }: { groupId: string }) { @@ -21,18 +24,18 @@ export function GroupView({ groupId }: { groupId: string }) { } function GroupCard({ group }: { group: Group }) { + const t = useT(); const { openUser } = useNav(); - const authorTags = (group.tags ?? []).filter((t) => t.startsWith("group_tag_")); + const ownerName = useSocial((s) => + group.ownerId ? s.users[group.ownerId]?.displayName : undefined, + ); + const authorTags = tagsWithPrefix(group.tags ?? [], "group_tag_"); return ( -
-
- -
- + } + body={

{group.name} @@ -40,12 +43,12 @@ function GroupCard({ group }: { group: Group }) {

{group.ownerId ? (

- owned by{" "} + {t("group:ownedByPrefix")}{" "}

) : null} @@ -56,71 +59,71 @@ function GroupCard({ group }: { group: Group }) { ) : null}
+ } + > +
+ } + label={t("group:stats.members")} + value={typeof group.memberCount === "number" ? compactNumber(group.memberCount) : "—"} + /> + } + label={t("group:stats.online")} + value={ + typeof group.onlineMemberCount === "number" + ? compactNumber(group.onlineMemberCount) + : "—" + } + live={(group.onlineMemberCount ?? 0) > 0} + /> + } + label={t("group:stats.languages")} + value={group.languages?.length ? group.languages.join(", ").toUpperCase() : "—"} + />
-
-
- } - label="Members" - value={typeof group.memberCount === "number" ? compactNumber(group.memberCount) : "—"} - /> - } - label="Online" - value={ - typeof group.onlineMemberCount === "number" - ? compactNumber(group.onlineMemberCount) - : "—" - } - live={(group.onlineMemberCount ?? 0) > 0} - /> - } - label="Languages" - value={group.languages?.length ? group.languages.join(", ").toUpperCase() : "—"} - /> -
+ {group.description ? ( +
+

+ {group.description} +

+
+ ) : null} - {group.description ? ( -
-

- {group.description} -

+ {group.rules ? ( +
+

+ {group.rules} +

+
+ ) : null} + +
+
+
+ {group.joinState ? ( + + ) : null} + {group.createdAt ? ( + + ) : null} + +
+
+ + {authorTags.length ? ( +
+
+ {authorTags.map((tag) => ( + {prettyTag(tag, "group_tag_")} + ))} +
) : null} - - {group.rules ? ( -
-

- {group.rules} -

-
- ) : null} - -
-
-
- {group.joinState ? : null} - {group.createdAt ? ( - - ) : null} - -
-
- - {authorTags.length ? ( -
-
- {authorTags.map((t) => ( - {prettyTag(t, "group_tag_")} - ))} -
-
- ) : null} -
-
+ ); } diff --git a/src/renderer/src/features/profile/ProfileView.tsx b/src/renderer/src/features/profile/ProfileView.tsx index 7dc8bea..98abb7c 100644 --- a/src/renderer/src/features/profile/ProfileView.tsx +++ b/src/renderer/src/features/profile/ProfileView.tsx @@ -31,7 +31,8 @@ import { WorldsSection, FavoriteWorldsSection, WorldSearch } from "./WorldsSecti import { GroupsSection } from "./GroupsSection"; import { LocationSection } from "./LocationSection"; import { COL, COL_WIDE } from "../../lib/layout"; -import { formatDate, formatDateTime, platformLabel, prettyTag } from "../../lib/format"; +import { HeroHeader } from "../shared/HeroHeader"; +import { formatDate, formatDateTime, platformLabel, prettyLink, prettyTag } from "../../lib/format"; import "./profile.css"; export function ProfileView({ target }: { target: "me" | string }) { @@ -64,13 +65,11 @@ function ProfileCard({ profile }: { profile: UserProfile }) { const showcasedBadges = (profile.badges ?? []).filter((b) => b.showcased); return ( -
-
- -
+
- + } + body={

{profile.displayName}

@@ -113,8 +113,9 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
) : null}
- - {canAdd ? ( + } + actions={ + canAdd ? (
@@ -130,11 +131,10 @@ function ProfileCard({ profile }: { profile: UserProfile }) {
- ) : null} - - -
- + ) : null + } + > +
) : null} - {menu ? ( + ); } @@ -392,14 +391,6 @@ function ProfileSkeleton() { ); } -function prettyLink(url: string): string { - try { - return new URL(url).hostname.replace(/^www\./, ""); - } catch { - return url; - } -} - function stateLabel(state: NonNullable, t: ReturnType): string { if (state === "online") return t("profile:state.online"); if (state === "active") return t("profile:state.active"); diff --git a/src/renderer/src/features/shared/HeroHeader.tsx b/src/renderer/src/features/shared/HeroHeader.tsx new file mode 100644 index 0000000..9858153 --- /dev/null +++ b/src/renderer/src/features/shared/HeroHeader.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from "react"; +import { COL_WIDE } from "../../lib/layout"; +import "../profile/profile.css"; + +export function HeroHeader({ + banner, + media, + body, + actions, + gap = "gap-5", + overlap = -64, + contentClassName = "mt-6 flex flex-col gap-5", + children, +}: { + banner?: string; + media: ReactNode; + body: ReactNode; + actions?: ReactNode; + gap?: string; + overlap?: number; + contentClassName?: string; + children: ReactNode; +}) { + return ( +
+
+ +
+ {media} + {body} + {actions} +
+ +
{children}
+
+ ); +} diff --git a/src/renderer/src/features/world/WorldView.tsx b/src/renderer/src/features/world/WorldView.tsx index 70a9af6..fe4c340 100644 --- a/src/renderer/src/features/world/WorldView.tsx +++ b/src/renderer/src/features/world/WorldView.tsx @@ -4,11 +4,12 @@ import type { World } from "../../../../shared/types/world"; import { Banner, Button, Fact, Section, Skeleton, StatTile, Tag } from "../../components/ui"; import { api } from "../../lib/api"; import { useAsync } from "../../lib/useAsync"; -import { compactNumber, formatDate, prettyTag } from "../../lib/format"; +import { compactNumber, formatDate, prettyTag, tagsWithPrefix } from "../../lib/format"; import { useWorlds } from "../../store/worlds"; import { useNav } from "../navigation/NavContext"; import { useT } from "../../lib/i18n"; import { COL_WIDE } from "../../lib/layout"; +import { HeroHeader } from "../shared/HeroHeader"; import { CreateInstanceModal } from "./CreateInstanceModal"; import "../profile/profile.css"; @@ -31,22 +32,20 @@ function WorldCard({ world }: { world: World }) { const players = world.occupants; return ( -
-
- -
+ {world.thumbnailImageUrl || world.imageUrl ? ( ) : null}
+ } + body={

{world.name}

- by{" "} + {t("world:detail.byPrefix")}{" "}

+ } + actions={ - + } + > + setCreateOpen(false)} /> - setCreateOpen(false)} - /> - -
-
+
} - label="Players now" + label={t("world:detail.stats.playersNow")} value={compactNumber(players)} live={players > 0} /> } - label="Favorites" + label={t("world:detail.stats.favorites")} value={compactNumber(world.favorites)} /> } - label="Visits" + label={t("world:detail.stats.visits")} value={world.visits ? compactNumber(world.visits) : "—"} /> - } label="Capacity" value={`${world.capacity}`} /> + } + label={t("world:detail.stats.capacity")} + value={`${world.capacity}`} + />
{world.description ? ( -
+

{world.description}

@@ -104,12 +104,12 @@ function WorldCard({ world }: { world: World }) { ) : null} {world.previewYoutubeId ? ( -
+