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");
+ });
+});