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 { redirect } from "next/navigation";
import { buyBadge } from "@/actions/draw-badge"; import { buyBadge } from "@/actions/draw-badge";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { CurrencyIcon } from "@/components/shared/currency-icon";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, User, WebsiteDrawbadges } from "@/lib/db"; import { db, User, WebsiteDrawbadges } from "@/lib/db";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
@@ -193,7 +194,7 @@ export default async function DrawBadgePage({
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
<h3 style={{ margin: 0 }}>{b.badgeName}</h3> <h3 style={{ margin: 0 }}>{b.badgeName}</h3>
<span className="currency"> <span className="currency">
<span className="coin credits">cr</span> <CurrencyIcon kind="credits" size={22} alt="" />
{price.toLocaleString()} {price.toLocaleString()}
</span> </span>
</div> </div>
+22 -15
View File
@@ -2,6 +2,10 @@ import { desc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; 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 { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db, User, UsersCurrency, UsersSettings } from "@/lib/db"; 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): // Currency type ids (see UsersCurrency in src/db/schema.ts):
// Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5. // Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5.
const TABS = [ const TABS = [
{ key: "credits", label: "Credits", coin: "credits" }, { key: "credits", label: "Credits" },
{ key: "diamonds", label: "Diamonds", coin: "diamonds" }, { key: "diamonds", label: "Diamonds" },
{ key: "duckets", label: "Duckets", coin: "duckets" }, { key: "duckets", label: "Duckets" },
{ key: "achievement", label: "Achievement", coin: "" }, { key: "achievement", label: "Achievement" },
{ key: "respects", label: "Respects", coin: "" }, { key: "respects", label: "Respects" },
{ key: "onlinetime", label: "Online Time", coin: "" }, { key: "onlinetime", label: "Online Time" },
] as const; ] as const;
type TabKey = (typeof TABS)[number]["key"]; type TabKey = (typeof TABS)[number]["key"];
@@ -155,7 +159,6 @@ export default async function LeaderboardPage({
const active: TabKey = TABS.some((tab) => tab.key === type) const active: TabKey = TABS.some((tab) => tab.key === type)
? (type as TabKey) ? (type as TabKey)
: "credits"; : "credits";
const activeTab = TABS.find((tab) => tab.key === active) ?? TABS[0];
const rows = await (active === "credits" const rows = await (active === "credits"
? loadCreditsRows() ? loadCreditsRows()
@@ -166,10 +169,10 @@ export default async function LeaderboardPage({
SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD], SETTINGS_FIELD[active as keyof typeof SETTINGS_FIELD],
)); ));
const valueOnly = const activeCurrency: CurrencyKind | null =
active === "achievement" || active === "credits" || active === "diamonds" || active === "duckets"
active === "respects" || ? active
active === "onlinetime"; : null;
return ( return (
<main style={{ display: "grid", gap: "1.5rem" }}> <main style={{ display: "grid", gap: "1.5rem" }}>
@@ -226,16 +229,20 @@ export default async function LeaderboardPage({
</Link> </Link>
</td> </td>
<td style={{ textAlign: "right" }}> <td style={{ textAlign: "right" }}>
{valueOnly ? ( {activeCurrency ? (
<strong>{formatValue(active, row.value)}</strong>
) : (
<span <span
className="currency" className="currency"
style={{ display: "inline-flex" }} style={{ display: "inline-flex" }}
> >
<span className={`coin ${activeTab.coin}`} /> <CurrencyIcon
kind={activeCurrency}
size={22}
alt=""
/>
{row.value.toLocaleString()} {row.value.toLocaleString()}
</span> </span>
) : (
<strong>{formatValue(active, row.value)}</strong>
)} )}
</td> </td>
</tr> </tr>
+2 -1
View File
@@ -7,6 +7,7 @@ import {
OnlineBadge, OnlineBadge,
RankBadge, RankBadge,
} from "@/components/public/ui"; } from "@/components/public/ui";
import { CurrencyIcon } from "@/components/shared/currency-icon";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
@@ -69,7 +70,7 @@ export default async function RankingsPage() {
<Link href={`/u/${u.username}`}>{u.username}</Link> <Link href={`/u/${u.username}`}>{u.username}</Link>
</h3> </h3>
<p className="currency" style={{ margin: "0 0 0.3rem" }}> <p className="currency" style={{ margin: "0 0 0.3rem" }}>
<span className="coin credits">c</span> <CurrencyIcon kind="credits" size={22} alt="" />
{u.credits.toLocaleString()} {u.credits.toLocaleString()}
</p> </p>
<OnlineBadge online={u.online === "1"} /> <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 type { CSSProperties } from "react";
import { buyShopArticle } from "@/actions/shop"; import { buyShopArticle } from "@/actions/shop";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { CurrencyIcon } from "@/components/shared/currency-icon";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db"; import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db";
import { excerpt } from "@/lib/format"; import { excerpt } from "@/lib/format";
@@ -277,19 +278,19 @@ export default async function ShopPage({
> >
{a.credits ? ( {a.credits ? (
<span className="currency"> <span className="currency">
<span className="coin credits">cr</span> <CurrencyIcon kind="credits" size={22} alt="" />
{a.credits.toLocaleString()} {a.credits.toLocaleString()}
</span> </span>
) : null} ) : null}
{a.duckets ? ( {a.duckets ? (
<span className="currency"> <span className="currency">
<span className="coin duckets">du</span> <CurrencyIcon kind="duckets" size={22} alt="" />
{a.duckets.toLocaleString()} {a.duckets.toLocaleString()}
</span> </span>
) : null} ) : null}
{a.diamonds ? ( {a.diamonds ? (
<span className="currency"> <span className="currency">
<span className="coin diamonds">di</span> <CurrencyIcon kind="diamonds" size={22} alt="" />
{a.diamonds.toLocaleString()} {a.diamonds.toLocaleString()}
</span> </span>
) : null} ) : null}
-9
View File
@@ -620,15 +620,6 @@ tbody tr:hover {
border: 1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent); border: 1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent);
color: var(--color-pill-text-readable, var(--color-text-readable)); 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 { .nav-credit-icon {
background: #e9b124 url("/assets/images/icons/currency/credits.png") center background: #e9b124 url("/assets/images/icons/currency/credits.png") center
no-repeat; 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");
});
});