Wrap next/link in a shared Link component that ships prefetch=false by default, so no route is ever prefetched (viewport or hover) anymore, and drop the DNS prefetch hint. Removes hidden background requests that were the source of intermittent issues.
124 lines
3.0 KiB
TypeScript
124 lines
3.0 KiB
TypeScript
import { asc, desc, eq, gte } from "drizzle-orm";
|
|
import { getTranslations } from "next-intl/server";
|
|
import Link from "@/components/link";
|
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
|
import { db, User, WebsiteTeams } from "@/lib/db";
|
|
import { siteSettings } from "@/lib/services/site-settings";
|
|
|
|
export const metadata = { title: "Staff" };
|
|
|
|
type StaffMember = {
|
|
username: string;
|
|
look: string;
|
|
rank: number;
|
|
motto: string;
|
|
};
|
|
|
|
type Team = {
|
|
id: string;
|
|
rankName: string;
|
|
badge: string | null;
|
|
jobDescription: string | null;
|
|
staffColor: string;
|
|
};
|
|
|
|
export default async function StaffPage() {
|
|
const t = await getTranslations("pages.staff");
|
|
const minStaffRank =
|
|
Number(await siteSettings.get("min_staff_rank", "7")) || 7;
|
|
|
|
// Independent queries — staff members and teams load concurrently.
|
|
const [staff, teams] = await Promise.all([
|
|
db
|
|
.select({
|
|
username: User.username,
|
|
look: User.look,
|
|
rank: User.rank,
|
|
motto: User.motto,
|
|
})
|
|
.from(User)
|
|
.where(gte(User.rank, minStaffRank))
|
|
.orderBy(desc(User.rank), asc(User.username))
|
|
.limit(100)
|
|
.catch((): StaffMember[] => []),
|
|
db
|
|
.select({
|
|
id: WebsiteTeams.id,
|
|
rankName: WebsiteTeams.rankName,
|
|
badge: WebsiteTeams.badge,
|
|
jobDescription: WebsiteTeams.jobDescription,
|
|
staffColor: WebsiteTeams.staffColor,
|
|
})
|
|
.from(WebsiteTeams)
|
|
.where(eq(WebsiteTeams.hiddenRank, false))
|
|
.orderBy(asc(WebsiteTeams.id))
|
|
.then((rows) => rows.map((row) => ({ ...row, id: String(row.id) })))
|
|
.catch((): Team[] => []),
|
|
]);
|
|
|
|
return (
|
|
<main style={{ display: "grid", gap: "1.5rem" }}>
|
|
<ContentCard icon="🛡️" title={t("title")} subtitle={t("subtitle")} />
|
|
|
|
{teams.length > 0 ? (
|
|
<ContentCard
|
|
icon="⭐"
|
|
title={t("ranksTitle")}
|
|
subtitle={t("ranksSubtitle")}
|
|
>
|
|
<div className="card-grid sm-2 lg-3">
|
|
{teams.map((team) => (
|
|
<div key={team.id} className="card">
|
|
<h3 style={{ margin: "0 0 0.25rem", color: team.staffColor }}>
|
|
{team.rankName}
|
|
</h3>
|
|
{team.jobDescription ? (
|
|
<p className="muted" style={{ margin: 0 }}>
|
|
{team.jobDescription}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ContentCard>
|
|
) : null}
|
|
|
|
<ContentCard
|
|
icon="👥"
|
|
title={t("membersTitle")}
|
|
subtitle={
|
|
staff.length === 0
|
|
? t("membersEmptySubtitle")
|
|
: t("membersSubtitle", { count: staff.length })
|
|
}
|
|
padded={staff.length === 0}
|
|
>
|
|
{staff.length === 0 ? (
|
|
<EmptyState icon="🛡️">{t("emptyState")}</EmptyState>
|
|
) : (
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{t("colUsername")}</th>
|
|
<th>{t("colRank")}</th>
|
|
<th>{t("colMotto")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{staff.map((u) => (
|
|
<tr key={u.username}>
|
|
<td>
|
|
<Link href={`/u/${u.username}`}>{u.username}</Link>
|
|
</td>
|
|
<td>{u.rank}</td>
|
|
<td className="muted">{u.motto || "—"}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</ContentCard>
|
|
</main>
|
|
);
|
|
}
|