fix: use currency icons in public balances
This commit is contained in:
1 parent
adc201bfb6
commit
06cd0cfe42
6 files changed
+72
-29
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user