Files
EpicNext-Cms/src/app/(site)/rares/page.tsx
T
openhands f285a7cd98
CI / check (push) Successful in 34s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m23s
Add performance optimizations and component refactors
- Cache read-heavy public API routes via redisCache (leaderboard, values,
  shop, articles, photos, guilds, teams, staff, users, home, radio, badges)
- Add single-flight and bounded-memory cache layer with unit tests
- Parallelize independent DB queries on search, rares, shop, staff, polls
  and profile pages
- Push radio points leaderboard aggregation to SQL with a LIMIT
- Split studio-client and import-furni-client into focused modules
- Clean up next.config.ts
2026-08-17 22:02:21 +02:00

110 lines
3.1 KiB
TypeScript

import { asc, count } from "drizzle-orm";
import Link from "next/link";
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";
export const metadata = { title: "Rare values" };
type CategoryRow = {
id: bigint;
name: string;
badge: string;
priority: number;
};
export default async function RareValuesPage() {
const t = await getTranslations("pages.rares");
// Categories, per-category rare counts and site settings are all
// independent — run them concurrently instead of sequentially.
const [categoryRows, grouped, badgesBase] = await Promise.all([
db
.select({
id: WebsiteRareValueCategories.id,
name: WebsiteRareValueCategories.name,
badge: WebsiteRareValueCategories.badge,
priority: WebsiteRareValueCategories.priority,
})
.from(WebsiteRareValueCategories)
.orderBy(
asc(WebsiteRareValueCategories.priority),
asc(WebsiteRareValueCategories.name),
)
.catch((): CategoryRow[] => []),
db
.select({
categoryId: WebsiteRareValues.categoryId,
value: count(),
})
.from(WebsiteRareValues)
.groupBy(WebsiteRareValues.categoryId)
.catch(() => []),
siteSettings.get("badges_path", ""),
]);
const categories = categoryRows;
// Per-category rare counts (grouped so we avoid N queries).
const counts = new Map<string, number>();
for (const g of grouped) {
counts.set(String(g.categoryId), Number(g.value));
}
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard icon="💎" title={t("title")} subtitle={t("subtitle")} />
{categories.length === 0 ? (
<ContentCard padded={false}>
<EmptyState icon="💎">{t("empty")}</EmptyState>
</ContentCard>
) : (
<div className="card-grid sm-2 lg-3">
{categories.map((c) => {
const id = String(c.id);
const rareCount = counts.get(id) ?? 0;
const badgeSrc =
badgesBase && c.badge ? `${badgesBase}/${c.badge}.gif` : "";
return (
<Link
key={id}
href={`/rares/${id}`}
className="content-card hover-lift"
style={{ color: "inherit", textDecoration: "none" }}
>
<div className="content-card-head">
{badgeSrc ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={badgeSrc}
alt={`${c.name} badge`}
width={36}
height={36}
style={{ flexShrink: 0 }}
/>
) : (
<span className="content-card-icon" aria-hidden>
💎
</span>
)}
<div className="content-card-head-text">
<p className="content-card-title">{c.name}</p>
<p className="content-card-subtitle">
{t("rareCount", { count: rareCount })}
</p>
</div>
</div>
<div className="content-card-body">
<span className="btn btn-outline">{t("viewRares")}</span>
</div>
</Link>
);
})}
</div>
)}
</main>
);
}