- Clone import: defer FurnitureData.json writes and append all entries in a single batched write instead of one read-modify-write per item, removing the main serialization bottleneck for large batches. - Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the clone client/route default from 2 to 6. - Add a flush hook to runSseBatch so callers can batch deferred work before batch_complete is emitted, and surface flush errors as an error event. - Enable Next.js Cache Components (instant: false opt-out) and silence the related build warnings in next.config.ts. - Switch isomorphic-dompurify to dompurify and refresh dependencies.
204 lines
5.1 KiB
TypeScript
204 lines
5.1 KiB
TypeScript
import { asc, eq } from "drizzle-orm";
|
|
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
|
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
|
|
// TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.
|
|
// See: https://nextjs.org/docs/app/guides/migrating-to-cache-components
|
|
export const instant = false;
|
|
|
|
type RareRow = {
|
|
id: bigint;
|
|
name: string;
|
|
creditValue: string | null;
|
|
currencyValue: string | null;
|
|
currencyType: string;
|
|
furnitureIcon: string;
|
|
itemId: number | null;
|
|
};
|
|
|
|
type CurrencyTranslator = (key: string) => string;
|
|
|
|
function prettyCurrency(type: string, t: CurrencyTranslator): string {
|
|
switch (type) {
|
|
case "0":
|
|
case "duckets":
|
|
return t("duckets");
|
|
case "5":
|
|
case "diamonds":
|
|
return t("diamonds");
|
|
case "-1":
|
|
case "credits":
|
|
return t("credits");
|
|
default:
|
|
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
}
|
|
}
|
|
|
|
export default async function RareCategoryPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ category: string }>;
|
|
}) {
|
|
const t = await getTranslations("pages.rareCategory");
|
|
const { category } = await params;
|
|
|
|
let categoryId: bigint;
|
|
try {
|
|
categoryId = BigInt(category);
|
|
} catch {
|
|
notFound();
|
|
}
|
|
|
|
let cat: { id: bigint; name: string; badge: string } | null = null;
|
|
try {
|
|
const [row] = await db
|
|
.select({
|
|
id: WebsiteRareValueCategories.id,
|
|
name: WebsiteRareValueCategories.name,
|
|
badge: WebsiteRareValueCategories.badge,
|
|
})
|
|
.from(WebsiteRareValueCategories)
|
|
.where(eq(WebsiteRareValueCategories.id, categoryId))
|
|
.limit(1);
|
|
cat = row ?? null;
|
|
} catch {
|
|
cat = null;
|
|
}
|
|
|
|
if (!cat) notFound();
|
|
|
|
let rares: RareRow[] = [];
|
|
try {
|
|
rares = await db
|
|
.select({
|
|
id: WebsiteRareValues.id,
|
|
name: WebsiteRareValues.name,
|
|
creditValue: WebsiteRareValues.creditValue,
|
|
currencyValue: WebsiteRareValues.currencyValue,
|
|
currencyType: WebsiteRareValues.currencyType,
|
|
furnitureIcon: WebsiteRareValues.furnitureIcon,
|
|
itemId: WebsiteRareValues.itemId,
|
|
})
|
|
.from(WebsiteRareValues)
|
|
.where(eq(WebsiteRareValues.categoryId, categoryId))
|
|
.orderBy(asc(WebsiteRareValues.name));
|
|
} catch {
|
|
rares = [];
|
|
}
|
|
|
|
const [iconsBase, badgesBase] = await Promise.all([
|
|
siteSettings.get("furniture_icons_path", ""),
|
|
siteSettings.get("badges_path", ""),
|
|
]);
|
|
const iconBase = iconsBase ?? "";
|
|
const badgeBase = badgesBase ?? "";
|
|
const badgeSrc =
|
|
badgeBase && cat.badge ? `${badgeBase}/${cat.badge}.gif` : "";
|
|
|
|
return (
|
|
<main style={{ display: "grid", gap: "1.5rem" }}>
|
|
<ContentCard
|
|
icon={badgeSrc ? undefined : "💎"}
|
|
title={
|
|
<span
|
|
style={{
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: "0.6rem",
|
|
}}
|
|
>
|
|
{badgeSrc ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={badgeSrc}
|
|
alt={`${cat.name} badge`}
|
|
width={32}
|
|
height={32}
|
|
/>
|
|
) : null}
|
|
{cat.name}
|
|
</span>
|
|
}
|
|
subtitle={t("rareCount", { count: rares.length })}
|
|
action={
|
|
<Link className="btn btn-outline" href="/rares">
|
|
{t("allRares")}
|
|
</Link>
|
|
}
|
|
padded={rares.length === 0}
|
|
>
|
|
{rares.length === 0 ? (
|
|
<EmptyState icon="💎">{t("empty")}</EmptyState>
|
|
) : (
|
|
<div style={{ padding: "0.25rem 1rem 0.75rem", overflowX: "auto" }}>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ width: 56 }}></th>
|
|
<th>{t("colRare")}</th>
|
|
<th>{t("colCredits")}</th>
|
|
<th>{t("colCurrency")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rares.map((r) => {
|
|
const iconSrc =
|
|
iconBase && r.furnitureIcon
|
|
? `${iconBase}/${r.furnitureIcon}`
|
|
: "";
|
|
return (
|
|
<tr key={String(r.id)}>
|
|
<td>
|
|
{iconSrc ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={iconSrc}
|
|
alt=""
|
|
width={40}
|
|
height={40}
|
|
style={{ objectFit: "contain" }}
|
|
/>
|
|
) : (
|
|
<span
|
|
className="avatar"
|
|
style={{
|
|
display: "inline-block",
|
|
width: 40,
|
|
height: 40,
|
|
}}
|
|
/>
|
|
)}
|
|
</td>
|
|
<td>
|
|
<strong>{r.name}</strong>
|
|
</td>
|
|
<td>
|
|
{r.creditValue && r.creditValue !== "" ? (
|
|
t("creditsValue", { value: r.creditValue })
|
|
) : (
|
|
<span className="muted">—</span>
|
|
)}
|
|
</td>
|
|
<td>
|
|
{r.currencyValue && r.currencyValue !== "" ? (
|
|
`${r.currencyValue} ${prettyCurrency(r.currencyType, t)}`
|
|
) : (
|
|
<span className="muted">—</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</ContentCard>
|
|
</main>
|
|
);
|
|
}
|