diff --git a/src/app/(site)/draw-badge/page.tsx b/src/app/(site)/draw-badge/page.tsx index e72d6c93..322a3807 100644 --- a/src/app/(site)/draw-badge/page.tsx +++ b/src/app/(site)/draw-badge/page.tsx @@ -2,6 +2,7 @@ import { desc, eq } from "drizzle-orm"; import { redirect } from "next/navigation"; import { buyBadge } from "@/actions/draw-badge"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; +import { CurrencyIcon } from "@/components/shared/currency-icon"; import { auth } from "@/lib/auth"; import { db, User, WebsiteDrawbadges } from "@/lib/db"; import { siteSettings } from "@/lib/services/site-settings"; @@ -193,7 +194,7 @@ export default async function DrawBadgePage({

{b.badgeName}

- cr + {price.toLocaleString()}
diff --git a/src/app/(site)/leaderboard/page.tsx b/src/app/(site)/leaderboard/page.tsx index 165a5c2f..c25b9326 100644 --- a/src/app/(site)/leaderboard/page.tsx +++ b/src/app/(site)/leaderboard/page.tsx @@ -2,6 +2,10 @@ import { desc, eq, inArray } from "drizzle-orm"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; +import { + CurrencyIcon, + type CurrencyKind, +} from "@/components/shared/currency-icon"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { db, User, UsersCurrency, UsersSettings } from "@/lib/db"; @@ -10,12 +14,12 @@ export const metadata = { title: "Leaderboard" }; // Currency type ids (see UsersCurrency in src/db/schema.ts): // Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5. const TABS = [ - { key: "credits", label: "Credits", coin: "credits" }, - { key: "diamonds", label: "Diamonds", coin: "diamonds" }, - { key: "duckets", label: "Duckets", coin: "duckets" }, - { key: "achievement", label: "Achievement", coin: "" }, - { key: "respects", label: "Respects", coin: "" }, - { key: "onlinetime", label: "Online Time", coin: "" }, + { key: "credits", label: "Credits" }, + { key: "diamonds", label: "Diamonds" }, + { key: "duckets", label: "Duckets" }, + { key: "achievement", label: "Achievement" }, + { key: "respects", label: "Respects" }, + { key: "onlinetime", label: "Online Time" }, ] as const; type TabKey = (typeof TABS)[number]["key"]; @@ -155,7 +159,6 @@ export default async function LeaderboardPage({ const active: TabKey = TABS.some((tab) => tab.key === type) ? (type as TabKey) : "credits"; - const activeTab = TABS.find((tab) => tab.key === active) ?? TABS[0]; const rows = await (active === "credits" ? loadCreditsRows() @@ -166,10 +169,10 @@ export default async function LeaderboardPage({ SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD], )); - const valueOnly = - active === "achievement" || - active === "respects" || - active === "onlinetime"; + const activeCurrency: CurrencyKind | null = + active === "credits" || active === "diamonds" || active === "duckets" + ? active + : null; return (
@@ -226,16 +229,20 @@ export default async function LeaderboardPage({ - {valueOnly ? ( - {formatValue(active, row.value)} - ) : ( + {activeCurrency ? ( - + {row.value.toLocaleString()} + ) : ( + {formatValue(active, row.value)} )} diff --git a/src/app/(site)/rankings/page.tsx b/src/app/(site)/rankings/page.tsx index 71056cb5..22b9b26e 100644 --- a/src/app/(site)/rankings/page.tsx +++ b/src/app/(site)/rankings/page.tsx @@ -7,6 +7,7 @@ import { OnlineBadge, RankBadge, } from "@/components/public/ui"; +import { CurrencyIcon } from "@/components/shared/currency-icon"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { db, User } from "@/lib/db"; @@ -69,7 +70,7 @@ export default async function RankingsPage() { {u.username}

- c + {u.credits.toLocaleString()}

diff --git a/src/app/(site)/shop/page.tsx b/src/app/(site)/shop/page.tsx index 8a25e62a..fe8514cb 100644 --- a/src/app/(site)/shop/page.tsx +++ b/src/app/(site)/shop/page.tsx @@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import { buyShopArticle } from "@/actions/shop"; import { ContentCard, EmptyState } from "@/components/public/ui"; +import { CurrencyIcon } from "@/components/shared/currency-icon"; import { auth } from "@/lib/auth"; import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db"; import { excerpt } from "@/lib/format"; @@ -277,19 +278,19 @@ export default async function ShopPage({ > {a.credits ? ( - cr + {a.credits.toLocaleString()} ) : null} {a.duckets ? ( - du + {a.duckets.toLocaleString()} ) : null} {a.diamonds ? ( - di + {a.diamonds.toLocaleString()} ) : null} diff --git a/src/app/globals.css b/src/app/globals.css index c8d036c1..182e3d0c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -620,15 +620,6 @@ tbody tr:hover { border: 1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent); color: var(--color-pill-text-readable, var(--color-text-readable)); } -.currency .coin { - width: 22px; - height: 22px; - border-radius: 999px; - display: grid; - place-items: center; - color: #fff; - font-size: 0.7rem; -} .nav-credit-icon { background: #e9b124 url("/assets/images/icons/currency/credits.png") center no-repeat; diff --git a/src/components/shared/public-currency-contract.test.tsx b/src/components/shared/public-currency-contract.test.tsx new file mode 100644 index 00000000..78e465cf --- /dev/null +++ b/src/components/shared/public-currency-contract.test.tsx @@ -0,0 +1,42 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { CurrencyIcon } from "./currency-icon"; + +const publicCurrencyFiles = [ + "src/app/(site)/rankings/page.tsx", + "src/app/(site)/leaderboard/page.tsx", + "src/app/(site)/shop/page.tsx", + "src/app/(site)/draw-badge/page.tsx", +]; + +function source(path: string): string { + return readFileSync(resolve(process.cwd(), path), "utf8"); +} + +describe("public currency presentation contract", () => { + it.each([ + ["credits", "/assets/images/icons/currency/credits.png"], + ["duckets", "/assets/images/icons/currency/duckets.png"], + ["diamonds", "/assets/images/icons/currency/diamonds.png"], + ] as const)("renders the %s asset", (kind, expectedSrc) => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain(expectedSrc); + expect(html).toContain('width="22"'); + expect(html).toContain('height="22"'); + expect(html).toContain('alt=""'); + }); + + it.each(publicCurrencyFiles)("removes placeholder coins from %s", (path) => { + const contents = source(path); + expect(contents).not.toMatch(/className=(?:"coin|\{`coin)/); + expect(contents).toContain(" { + expect(source("src/app/globals.css")).not.toContain(".currency .coin"); + }); +});