From 31691c1c7d53a4be9b6099583b04eeb04b368e3c Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 6 Sep 2026 10:56:20 +0200 Subject: [PATCH] feat(profile): organize profile sections and use native currency icons --- src/app/(site)/u/[username]/page.tsx | 654 +++++++++--------- src/app/(site)/u/[username]/profile-image.tsx | 51 ++ .../(site)/u/[username]/profile.module.css | 176 +++++ src/messages/en.json | 8 + src/messages/it.json | 8 + 5 files changed, 557 insertions(+), 340 deletions(-) create mode 100644 src/app/(site)/u/[username]/profile-image.tsx create mode 100644 src/app/(site)/u/[username]/profile.module.css diff --git a/src/app/(site)/u/[username]/page.tsx b/src/app/(site)/u/[username]/page.tsx index 2ad4e03f..9bf8ecaf 100644 --- a/src/app/(site)/u/[username]/page.tsx +++ b/src/app/(site)/u/[username]/page.tsx @@ -6,12 +6,8 @@ import type { CSSProperties } from "react"; import { postGuestbook } from "@/actions/guestbook"; import { sendFriendRequest } from "@/actions/social"; import Link from "@/components/link"; -import { - ContentCard, - EmptyState, - OnlineBadge, - StatBlock, -} from "@/components/public/ui"; +import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; +import { CurrencyIcon } from "@/components/shared/currency-icon"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { auth } from "@/lib/auth"; import { @@ -23,10 +19,13 @@ import { User, UsersBadges, UsersCurrency, + WebsiteBadges, WebsiteUserGuestbooks, } from "@/lib/db"; import { avatarImageUrl } from "@/lib/format"; import { formatDate } from "@/lib/format-date"; +import styles from "./profile.module.css"; +import { ProfileImage } from "./profile-image"; type SearchParams = Promise<{ friend?: string; @@ -260,32 +259,26 @@ export default async function ProfilePage({ const wallet = [ { key: "credits", - icon: "๐Ÿ’ฐ", label: t("statCredits"), value: user.credits, - color: "var(--color-primary-readable, var(--color-primary))", }, { key: "duckets", - icon: "๐Ÿช™", label: t("statDuckets"), value: ducketsAmount, - color: "var(--color-accent-readable, var(--color-accent))", }, { key: "diamonds", - icon: "๐Ÿ’Ž", label: t("statDiamonds"), value: diamondsAmount, - color: "var(--color-text-readable, var(--color-text))", }, - ]; + ] as const; // Resolve friend usernames/looks and the guestbook author usernames/looks in - // two parallel queries (the ids come from the queries above). + // parallel queries (the ids come from the queries above). const friendIds = Array.from(new Set(friendships.map((f) => f.userTwoId))); const authorIds = Array.from(new Set(guestbook.map((g) => g.userId))); - const [friendUsers, authors] = await Promise.all([ + const [friendUsers, authors, badgeDetails] = await Promise.all([ friendIds.length ? db .select({ @@ -308,15 +301,28 @@ export default async function ProfilePage({ .where(inArray(User.id, authorIds)) .catch(() => []) : [], + badges.length + ? db + .select() + .from(WebsiteBadges) + .where( + inArray( + WebsiteBadges.badgeKey, + badges.map((b) => b.badgeCode), + ), + ) + .catch(() => []) + : [], ]); const friendById = new Map(friendUsers.map((f) => [f.id, f])); const friends = friendships .map((f) => friendById.get(f.userTwoId)) .filter((f): f is NonNullable => Boolean(f)); const authorById = new Map(authors.map((a) => [a.id, a])); + const badgeByCode = new Map(badgeDetails.map((b) => [b.badgeKey, b])); return ( -
+
{friend === "sent" ? (
{t("success.sent")} @@ -334,7 +340,7 @@ export default async function ProfilePage({ ) : null} {/* โ”€โ”€ Profile header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - +
{/* eslint-disable-next-line @next/next/no-img-element */} - {`${user.username} -
+

{user.username}

{user.motto || {t("noMotto")}}

- +
+ + + {t("statRegistered")}: {registered} + +
{isLoggedIn && !isSelf ? (
@@ -384,329 +396,291 @@ export default async function ProfilePage({
) : null}
-
- - -
- {/* โ”€โ”€ Wallet / currencies โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - -
- {wallet.map((w) => ( -
- - {w.icon} - -
-
- {w.value.toLocaleString()} -
-
- {w.label} -
-
-
- ))} -
-
- - {/* โ”€โ”€ Friends โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - - {friends.length === 0 ? ( - {t("friendsEmpty")} - ) : ( -
- {friends.map((f) => ( - - - - {f.username} - - - ))} -
- )} -
- - {/* โ”€โ”€ Rooms โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - - {rooms.length === 0 ? ( - {t("roomsEmpty")} - ) : ( -
- {rooms.map((r) => ( -
-
- - {"๐Ÿ "} - - - {r.name || t("roomUntitled")} - -
-
- - {"๐Ÿ‘ค"} {r.users}/{r.usersMax} - - {r.state} -
-
- ))} -
- )} -
- - {/* โ”€โ”€ Badges โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - - {badges.length === 0 ? ( - {t("badgesEmpty")} - ) : ( -
- {badges.map((b) => ( - /* eslint-disable-next-line @next/next/no-img-element */ - {b.badgeCode} - ))} -
- )} -
- - {/* โ”€โ”€ Photos โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - - {photos.length === 0 ? ( - {t("photosEmpty")} - ) : ( -
- {photos.map((p) => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`by -

- {formatDate(new Date(p.timestamp * 1000), "date")} -

-
- ))} -
- )} -
- - {/* โ”€โ”€ Guestbook โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} - - {isLoggedIn ? ( -
- {/* Profile owner id drives the write target; the author is taken from - the session inside the action, never from this form. */} - - - -