fix: use currency icons in public balances
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 46s

This commit is contained in:
Simo committed 2026-08-10 18:32:17 +02:00
1 parent adc201bfb6
commit 06cd0cfe42
6 files changed
+72 -29

No files matched your search

+2 -1
View File
@@ -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({
<div style={{ minWidth: 0 }}>
<h3 style={{ margin: 0 }}>{b.badgeName}</h3>
<span className="currency">
<span className="coin credits">cr</span>
<CurrencyIcon kind="credits" size={22} alt="" />
{price.toLocaleString()}
</span>
</div>
+22 -15
View File
@@ -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 (
<main style={{ display: "grid", gap: "1.5rem" }}>
@@ -226,16 +229,20 @@ export default async function LeaderboardPage({
</Link>
</td>
<td style={{ textAlign: "right" }}>
{valueOnly ? (
<strong>{formatValue(active, row.value)}</strong>
) : (
{activeCurrency ? (
<span
className="currency"
style={{ display: "inline-flex" }}
>
<span className={`coin ${activeTab.coin}`} />
<CurrencyIcon
kind={activeCurrency}
size={22}
alt=""
/>
{row.value.toLocaleString()}
</span>
) : (
<strong>{formatValue(active, row.value)}</strong>
)}
</td>
</tr>
+2 -1
View File
@@ -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() {
<Link href={`/u/${u.username}`}>{u.username}</Link>
</h3>
<p className="currency" style={{ margin: "0 0 0.3rem" }}>
<span className="coin credits">c</span>
<CurrencyIcon kind="credits" size={22} alt="" />
{u.credits.toLocaleString()}
</p>
<OnlineBadge online={u.online === "1"} />
+4 -3
View File
@@ -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 ? (
<span className="currency">
<span className="coin credits">cr</span>
<CurrencyIcon kind="credits" size={22} alt="" />
{a.credits.toLocaleString()}
</span>
) : null}
{a.duckets ? (
<span className="currency">
<span className="coin duckets">du</span>
<CurrencyIcon kind="duckets" size={22} alt="" />
{a.duckets.toLocaleString()}
</span>
) : null}
{a.diamonds ? (
<span className="currency">
<span className="coin diamonds">di</span>
<CurrencyIcon kind="diamonds" size={22} alt="" />
{a.diamonds.toLocaleString()}
</span>
) : null}
-9
View File
@@ -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;
@@ -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(
<CurrencyIcon kind={kind} size={22} alt="" />,
);
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("<CurrencyIcon");
});
it("removes the obsolete coin CSS contract", () => {
expect(source("src/app/globals.css")).not.toContain(".currency .coin");
});
});