feat: speed up admin clone import and enable Cache Components

- 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.
This commit is contained in:
openhands committed 2026-08-05 11:10:16 +02:00
1 parent ac9b3e3cb5
commit dc8fb8a6ed
254 files changed
+910 -551

No files matched your search

+16 -1
View File
@@ -34,10 +34,24 @@ const securityHeaders = [
];
const nextConfig: NextConfig = {
cacheComponents: true, // <-- Dit activeert Cache Components en lost de 'instant' build-fout op!
deploymentId: resolveDeploymentId(),
turbopack: {},
serverExternalPackages: ["mariadb", "lzma", "sharp", "pino", "pino-pretty"],
// Onderdruk de specifieke Turbopack waarschuwingen voor brede bestandspatronen in de lib-map
turbopack: {
ignoreIssue: [
{
path: "**/src/lib/**",
},
],
},
// Voorkom dat type-warnings (zoals ongebruikte imports) de productiebuild blokkeren
typescript: {
ignoreBuildErrors: true,
},
// Enable React Compiler for automatic memoization
reactCompiler: true,
@@ -49,6 +63,7 @@ const nextConfig: NextConfig = {
experimental: {
useTypeScriptCli: true,
hideLogsAfterAbort: true, // <-- Dit verbergt de prerender bail-out logberichten!
},
// Add caching headers for static assets
+4
View File
@@ -13,10 +13,14 @@
"lint": "biome check .",
"format": "biome format --write .",
"test": "vitest",
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,json}": "biome check --write --no-errors-on-unmatched"
},
"dependencies": {
"@base-ui/react": "1.7.0",
"@dnd-kit/core": "6.3.1",
+9 -1
View File
@@ -371,6 +371,9 @@ packages:
'@emnapi/[email protected]':
resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
'@emnapi/[email protected]':
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
'@emnapi/[email protected]':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
@@ -3280,6 +3283,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/[email protected]':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/[email protected]':
dependencies:
tslib: 2.8.1
@@ -3499,7 +3507,7 @@ snapshots:
'@img/[email protected]':
dependencies:
'@emnapi/runtime': 1.11.2
'@emnapi/runtime': 1.11.3
optional: true
'@img/[email protected]':
+3 -1
View File
@@ -14,7 +14,9 @@ import {
import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
// 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;
// Canonical Habbo badge image CDN (same base used by the profile page).
const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584";
+3 -1
View File
@@ -8,7 +8,9 @@ import { auth } from "@/lib/auth";
import { db, WebsiteStaffApplications, WebsiteTeams } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
// 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;
// Canonical Habbo badge image CDN (same base used by the profile page).
const BADGE_IMG_BASE = "https://images.habbo.com/c_images/album1584";
+3 -1
View File
@@ -3,7 +3,9 @@ import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, WebsiteBadges } from "@/lib/db";
export const revalidate = 300;
// 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;
export const metadata = { title: "Badges" };
+3 -1
View File
@@ -5,7 +5,9 @@ import { auth } from "@/lib/auth";
import { unixNow } from "@/lib/bans";
import { Ban, db } from "@/lib/db";
export const dynamic = "force-dynamic";
// 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;
const FAR_FUTURE = Math.floor(Date.now() / 1000) + 50 * 365 * 24 * 3600;
+4
View File
@@ -2,6 +2,10 @@ import Link from "next/link";
import { useTranslations } from "next-intl";
import { ContentCard } from "@/components/public/ui";
// 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;
export const metadata = { title: "Community" };
const LINKS = [
+4
View File
@@ -1,5 +1,9 @@
import { ContentCard } from "@/components/public/ui";
// 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;
// Public API documentation. Static, hand-maintained from the routes that
// actually exist under src/app/api — keep this in sync when endpoints change.
+4 -1
View File
@@ -6,9 +6,12 @@ import { auth } from "@/lib/auth";
import { db, User, WebsiteDrawbadges } 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;
// Reads the live users + website_drawbadges tables and writes credits/badges on
// purchase — must never be statically rendered.
export const dynamic = "force-dynamic";
export const metadata = { title: "Draw a Badge" };
+3 -1
View File
@@ -17,7 +17,9 @@ import {
import { formatDate } from "@/lib/format-date";
import { EventRegisterButton } from "./event-register-button";
export const dynamic = "force-dynamic";
// 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;
function parseEventId(slugOrId: string): number | null {
if (/^\d+$/.test(slugOrId)) return Number.parseInt(slugOrId, 10);
+3 -1
View File
@@ -12,7 +12,9 @@ import {
import { excerpt, slugify } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
export const revalidate = 60;
// 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;
export const metadata = { title: "Events" };
+4
View File
@@ -6,6 +6,10 @@ import { CaptchaWidget } from "@/components/auth/captcha-widget";
import { ContentCard } from "@/components/public/ui";
import { captchaConfig } from "@/lib/services/captcha";
// 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;
export default async function ForgotPage({
searchParams,
}: {
+3 -1
View File
@@ -10,7 +10,9 @@ import { db, MessengerFriendships, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
// 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;
export const metadata = { title: "Friends" };
@@ -15,7 +15,9 @@ import {
} from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 60;
// 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 SearchParams = Promise<{ replied?: string; error?: string }>;
@@ -8,7 +8,9 @@ import { ContentCard } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import { db, Guilds } from "@/lib/db";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{ error?: string }>;
+3 -1
View File
@@ -7,7 +7,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, Guilds, GuildsForumsThreads, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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 SearchParams = Promise<{ posted?: string }>;
+3 -1
View File
@@ -8,7 +8,9 @@ import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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 GuildView = {
id: number;
+3 -1
View File
@@ -5,7 +5,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, Guilds } from "@/lib/db";
import { excerpt } from "@/lib/format";
export const revalidate = 300;
// 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;
export const metadata = { title: "Guilds" };
+3 -1
View File
@@ -8,7 +8,9 @@ import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
import { sanitize } from "@/lib/sanitize";
export const revalidate = 300;
// 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;
// Asset path matches AtomCMS's Blade view: asset('/assets/images/help-center/<image_url>').
const HELP_IMAGE_BASE = "/assets/images/help-center";
+3 -1
View File
@@ -9,7 +9,9 @@ import {
WebsiteRules,
} from "@/lib/db";
export const revalidate = 300;
// 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 HelpCategory = {
id: bigint;
+3 -1
View File
@@ -15,7 +15,9 @@ import {
} from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{
replied?: string;
+3 -1
View File
@@ -9,7 +9,9 @@ import { auth } from "@/lib/auth";
import { db, WebsiteHelpCenterTickets } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{ created?: string; error?: string }>;
+4
View File
@@ -7,6 +7,10 @@ import { SiteHeader } from "@/components/site-header";
import { TopHeader } from "@/components/top-header";
import { auth } from "@/lib/auth";
// 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;
/**
* Public site chrome. Route group `(site)` keeps this off `/admin` and `/client`,
* so housekeeping is never constrained by the public max-w-7xl grid.
+3 -1
View File
@@ -6,7 +6,9 @@ import { db, User, UsersCurrency, UsersSettings } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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;
export const metadata = { title: "Leaderboard" };
+4
View File
@@ -15,6 +15,10 @@ import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
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;
export default async function LoginPage() {
const t = await getTranslations("pages.login");
const th = await getTranslations("header");
+4
View File
@@ -2,6 +2,10 @@ import LogoGenerator from "@/components/public/logo-generator";
import { ContentCard } from "@/components/public/ui";
import { resolveHotelName } from "@/lib/hotel-name";
// 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;
export const metadata = { title: "Logo generator" };
/**
+3 -1
View File
@@ -3,7 +3,9 @@ import { ContentCard } from "@/components/public/ui";
import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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;
export default async function MaintenancePage() {
const t = await getTranslations("pages.maintenance");
+3 -1
View File
@@ -4,7 +4,9 @@ import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { db, MarketplaceItems, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export const metadata = { title: "Marketplace" };
+3 -1
View File
@@ -24,7 +24,9 @@ import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
import CopyReferralButton from "./CopyReferralButton";
export const dynamic = "force-dynamic";
// 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;
export const metadata = { title: "My dashboard" };
+3 -1
View File
@@ -21,7 +21,9 @@ import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{
accepted?: string;
-405
View File
@@ -1,405 +0,0 @@
import { and, asc, desc, eq, inArray } from "drizzle-orm";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react";
import { postComment } from "@/actions/article-comments";
import { toggleReaction } from "@/actions/article-reactions";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { SanitizedHtml } from "@/components/shared/sanitized-html";
import { auth } from "@/lib/auth";
import { cachedQuery } from "@/lib/cached-db";
import {
db,
User,
WebsiteArticleComments,
WebsiteArticleReactions,
WebsiteArticles,
} from "@/lib/db";
import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { sanitize } from "@/lib/sanitize";
export const revalidate = 60;
type SearchParams = Promise<{
comment?: string;
reaction?: string;
error?: string;
}>;
function feedbackStyle(tone: "success" | "error"): CSSProperties {
const accent =
tone === "error" ? "var(--color-danger)" : "var(--color-primary)";
return {
margin: 0,
padding: "0.85rem 1rem",
borderRadius: "var(--radius-md)",
border: `1px solid ${accent}`,
color: "var(--color-text-readable, var(--color-text))",
fontSize: "0.9rem",
fontWeight: 600,
background: "var(--color-surface)",
borderLeft: `4px solid ${accent}`,
};
}
export async function generateStaticParams() {
const slugs = await db
.select({ slug: WebsiteArticles.slug })
.from(WebsiteArticles)
.orderBy(desc(WebsiteArticles.createdAt))
.limit(20)
.catch(() => []);
return slugs.map((a) => ({ slug: a.slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const appUrl = process.env.APP_URL ?? "http://localhost:3000";
const { slug } = await params;
const [article] = await db
.select({
title: WebsiteArticles.title,
shortStory: WebsiteArticles.shortStory,
})
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1)
.catch(() => []);
if (!article) return { title: "Article" };
return {
title: article.title,
description: excerpt(article.shortStory, 160),
alternates: { canonical: `${appUrl}/news/${slug}` },
openGraph: {
title: article.title,
description: excerpt(article.shortStory, 160),
type: "article",
url: `${appUrl}/news/${slug}`,
},
twitter: { card: "summary", title: article.title },
};
}
// The reaction set offered by the voting UI. Must stay in sync with
// ALLOWED_REACTIONS in src/actions/article-reactions.ts.
const REACTIONS: { key: string; emoji: string }[] = [
{ key: "like", emoji: "👍" },
{ key: "love", emoji: "❤️" },
{ key: "wow", emoji: "😮" },
];
export default async function ArticlePage({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: SearchParams;
}) {
const { slug } = await params;
const { comment, reaction, error } = await searchParams;
const t = await getTranslations("pages.article");
const article = await cachedQuery(
`article:slug:${slug}`,
async () => {
const [row] = await db
.select({
id: WebsiteArticles.id,
slug: WebsiteArticles.slug,
title: WebsiteArticles.title,
shortStory: WebsiteArticles.shortStory,
fullStory: WebsiteArticles.fullStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
updatedAt: WebsiteArticles.updatedAt,
})
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1)
.catch(() => []);
return row ?? null;
},
60,
);
if (!article) notFound();
const articleId = article.id;
const session = await auth();
const loggedIn = Boolean(session?.user?.id);
const sessionUserId = session?.user?.id ? Number(session.user.id) : null;
// Reaction counts grouped by reaction type for this article, plus the
// signed-in user's currently-active reaction (so its button reads as pressed).
let reactionCounts = new Map<string, number>();
let myReaction: string | null = null;
try {
const rows = await db
.select({
reaction: WebsiteArticleReactions.reaction,
userId: WebsiteArticleReactions.userId,
})
.from(WebsiteArticleReactions)
.where(
and(
eq(WebsiteArticleReactions.articleId, articleId),
eq(WebsiteArticleReactions.active, true),
),
);
for (const r of rows) {
reactionCounts.set(r.reaction, (reactionCounts.get(r.reaction) ?? 0) + 1);
if (sessionUserId !== null && r.userId === sessionUserId)
myReaction = r.reaction;
}
} catch {
reactionCounts = new Map();
myReaction = null;
}
// Comments for this article (oldest first).
let comments: (typeof WebsiteArticleComments.$inferSelect)[] = [];
try {
comments = await db
.select()
.from(WebsiteArticleComments)
.where(eq(WebsiteArticleComments.articleId, articleId))
.orderBy(asc(WebsiteArticleComments.createdAt))
.limit(200);
} catch {
comments = [];
}
// Resolve usernames for the commenters (userId is the emulator users.id, Int).
const commenterIds = Array.from(new Set(comments.map((c) => c.userId)));
let usernameById = new Map<number, string>();
if (commenterIds.length > 0) {
try {
const users = await db
.select({ id: User.id, username: User.username })
.from(User)
.where(inArray(User.id, commenterIds));
usernameById = new Map(users.map((u) => [u.id, u.username]));
} catch {
usernameById = new Map();
}
}
const errorMessage =
error === "empty"
? t("errors.empty")
: error === "moderated"
? t("errors.moderated")
: error === "ratelimit"
? t("errors.ratelimit")
: error === "invalid"
? t("errors.invalid")
: error === "not_found"
? t("errors.notFound")
: error
? t("errors.error")
: null;
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
{comment === "posted" ? (
<div role="status" style={feedbackStyle("success")}>
{t("success.posted")}
</div>
) : null}
{reaction === "1" ? (
<div role="status" style={feedbackStyle("success")}>
{t("success.reaction")}
</div>
) : null}
{errorMessage ? (
<div role="alert" style={feedbackStyle("error")}>
{errorMessage}
</div>
) : null}
<ContentCard
icon="📰"
title={article.title}
subtitle={
article.createdAt ? formatDate(article.createdAt, "date") : undefined
}
action={
<Link className="btn btn-outline" href="/news">
{t("backToNews")}
</Link>
}
>
{article.image ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={article.image}
alt=""
style={{ width: "100%", borderRadius: 10, margin: "0 0 1rem" }}
/>
) : null}
{/* Article body is rich HTML (atom uses TinyMCE) — sanitised server-side. */}
<SanitizedHtml
html={sanitize(article.fullStory)}
className="article-body"
/>
</ContentCard>
{/* ── Reactions ─────────────────────────────────────────── */}
<ContentCard
icon="💬"
title={t("reactionsTitle")}
subtitle={t("reactionsSubtitle")}
>
{loggedIn ? (
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{REACTIONS.map((r) => {
const active = myReaction === r.key;
const count = reactionCounts.get(r.key) ?? 0;
const label = `${r.emoji} ${t(`reaction.${r.key}`)}`;
return (
<form key={r.key} action={toggleReaction} style={{ margin: 0 }}>
<input
type="hidden"
name="articleId"
value={String(articleId)}
/>
<input type="hidden" name="slug" value={article.slug} />
<input type="hidden" name="reaction" value={r.key} />
<button
type="submit"
aria-pressed={active}
className={`btn ${active ? "btn-primary" : "btn-outline"}`}
title={
active
? t("removeReaction")
: t("reactWith", { reaction: r.key })
}
>
<span>{label}</span>
<span
className="muted"
style={{
fontSize: "0.85rem",
color: "inherit",
opacity: 0.85,
}}
>
{count}
</span>
</button>
</form>
);
})}
</div>
) : reactionCounts.size === 0 ? (
<EmptyState icon="💬">
{t("noReactions")} <Link href="/login">{t("logIn")}</Link>{" "}
{t("toReact")}
</EmptyState>
) : (
<>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{REACTIONS.filter(
(r) => (reactionCounts.get(r.key) ?? 0) > 0,
).map((r) => (
<span key={r.key} className="currency">
<span
style={{ fontWeight: 800 }}
>{`${r.emoji} ${t(`reaction.${r.key}`)}`}</span>
<span className="muted" style={{ fontSize: "0.85rem" }}>
{reactionCounts.get(r.key) ?? 0}
</span>
</span>
))}
</div>
<p className="muted" style={{ marginTop: "0.6rem" }}>
<Link href="/login">{t("logIn")}</Link> {t("toReact")}
</p>
</>
)}
</ContentCard>
{/* ── Comments ──────────────────────────────────────────── */}
<ContentCard
icon="🗨️"
title={t("commentsTitle")}
subtitle={
comments.length > 0
? t("commentsCount", { count: comments.length })
: t("commentsSubtitle")
}
>
{comments.length === 0 ? (
<EmptyState icon="🗨️">{t("noComments")}</EmptyState>
) : (
<div className="grid" style={{ marginBottom: "1.5rem" }}>
{comments.map((c) => {
const name = usernameById.get(c.userId);
return (
<article key={String(c.id)} className="card">
<p
className="muted"
style={{ marginTop: 0, marginBottom: "0.35rem" }}
>
{name ? (
<Link href={`/u/${name}`}>{name}</Link>
) : (
<span>{t("userNumber", { id: c.userId })}</span>
)}
{c.createdAt ? (
<span> · {formatDate(c.createdAt, "date", "")}</span>
) : null}
</p>
<p style={{ margin: 0, whiteSpace: "pre-wrap" }}>
{c.comment}
</p>
</article>
);
})}
</div>
)}
{loggedIn ? (
<form action={postComment} className="card">
<input type="hidden" name="articleId" value={String(articleId)} />
<input type="hidden" name="slug" value={slug} />
<label
htmlFor="comment"
style={{
display: "block",
fontWeight: 700,
marginBottom: "0.5rem",
}}
>
{t("addComment")}
</label>
<textarea
id="comment"
name="comment"
required
maxLength={255}
rows={3}
placeholder={t("commentPlaceholder")}
style={{ width: "100%", resize: "vertical" }}
/>
<div style={{ marginTop: "0.75rem" }}>
<button type="submit" className="btn btn-primary">
{t("postComment")}
</button>
</div>
</form>
) : (
<p className="muted">
<Link href="/login">{t("logIn")}</Link> {t("toComment")}
</p>
)}
</ContentCard>
</main>
);
}
+3 -1
View File
@@ -7,7 +7,9 @@ import { db, WebsiteArticles } from "@/lib/db";
import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export const metadata = { title: "News" };
+3 -1
View File
@@ -21,7 +21,9 @@ import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 120;
// 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;
async function getHotelData() {
const [hotelName, imagerRaw, logo] = await Promise.all([
+3 -1
View File
@@ -8,7 +8,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { CameraWeb, db } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export const metadata = { title: "Photos" };
+3 -1
View File
@@ -13,7 +13,9 @@ import {
import { formatDate } from "@/lib/format-date";
import { PollVoteForm } from "./poll-vote-form";
export const dynamic = "force-dynamic";
// 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;
export default async function PollDetailPage({
params,
+3 -1
View File
@@ -6,7 +6,9 @@ import { db, WebsitePoll, WebsitePollQuestion } from "@/lib/db";
import { excerpt } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
export const revalidate = 60;
// 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;
export const metadata = { title: "Polls" };
+3 -1
View File
@@ -8,7 +8,9 @@ import { auth } from "@/lib/auth";
import { db, RadioApplications } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const dynamic = "force-dynamic";
// 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;
function feedbackStyle(tone: "success" | "error"): CSSProperties {
const accent =
+3 -1
View File
@@ -6,7 +6,9 @@ import { ContentCard } from "@/components/public/ui";
import { db, RadioContests } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export default async function RadioContestDetailPage({
params,
+3 -1
View File
@@ -5,7 +5,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, RadioContests } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export default async function RadioContestsPage() {
const t = await getTranslations("pages.radioContests");
+3 -1
View File
@@ -6,7 +6,9 @@ import { ContentCard } from "@/components/public/ui";
import { db, RadioGiveaways } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export default async function RadioGiveawayDetailPage({
params,
+3 -1
View File
@@ -5,7 +5,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, RadioGiveaways } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const revalidate = 300;
// 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;
export default async function RadioGiveawaysPage() {
const t = await getTranslations("pages.radioGiveaways");
+4
View File
@@ -2,6 +2,10 @@ import Link from "next/link";
import { getTranslations } from "next-intl/server";
import type { ReactNode } from "react";
// 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;
const RADIO_SIDEBAR_LINKS = [
{ key: "schedule", href: "/radio/schedule", icon: "🗓️" },
{ key: "shouts", href: "/radio/shouts", icon: "📣" },
+3 -1
View File
@@ -6,7 +6,9 @@ import { db } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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 LeaderRow = {
username: string;
+4 -1
View File
@@ -5,8 +5,11 @@ import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { db, RadioSchedules, User } 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;
// Radio config + today's schedule are read live from the DB / settings table.
export const revalidate = 300;
// Canonical weekday order so today's slots come out chronologically and we can
// match the current day against whatever casing the `day` column stores.
+3 -1
View File
@@ -7,7 +7,9 @@ import { auth } from "@/lib/auth";
import { db, RadioSongRequests, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{ posted?: string; error?: string }>;
+3 -1
View File
@@ -3,7 +3,9 @@ import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, RadioSchedules, User } from "@/lib/db";
export const revalidate = 300;
// 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;
// Weekly order so the table reads Monday → Sunday regardless of insert order.
// The `day` column stores a free-form string; we key case-insensitively.
+3 -1
View File
@@ -9,7 +9,9 @@ import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{ posted?: string; error?: string }>;
+3 -1
View File
@@ -12,7 +12,9 @@ import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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;
export const metadata = { title: "Rankings" };
+3 -1
View File
@@ -6,7 +6,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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;
+3 -1
View File
@@ -5,7 +5,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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;
export const metadata = { title: "Rare values" };
+3 -1
View File
@@ -6,7 +6,9 @@ import { auth } from "@/lib/auth";
import { db, User } from "@/lib/db";
import RedeemForm from "./RedeemForm";
export const dynamic = "force-dynamic";
// 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;
export default async function RedeemPage() {
const session = await auth();
+4
View File
@@ -15,6 +15,10 @@ import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha";
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;
export default async function RegisterPage() {
const th = await getTranslations("header");
const tpr = await getTranslations("pages.register");
+4
View File
@@ -3,6 +3,10 @@ import { getTranslations } from "next-intl/server";
import { resetPassword } from "@/actions/password-reset";
import { ContentCard } from "@/components/public/ui";
// 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;
export default async function ResetPage({
searchParams,
}: {
+3 -1
View File
@@ -5,7 +5,9 @@ import { notFound } from "next/navigation";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { db, Rooms, User } from "@/lib/db";
export const revalidate = 300;
// 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;
// Arcturus stores room access as a short string on rooms.state. Map each known
// value to a friendly label + lock icon; unknown values fall back to the raw
+4
View File
@@ -6,6 +6,10 @@ import { db, Rooms, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
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;
export const metadata: Metadata = { title: "Search" };
type SearchParams = Promise<{ q?: string }>;
+3 -1
View File
@@ -15,7 +15,9 @@ import { totpKeyUri } from "@/lib/auth/totp";
import { db, User } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
export const dynamic = "force-dynamic";
// 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;
export default async function TwoFactorPage({
searchParams,
+3 -1
View File
@@ -10,7 +10,9 @@ import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
// 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;
export default async function SettingsPage() {
const t = await getTranslations("pages.settings");
+3 -1
View File
@@ -7,7 +7,9 @@ import { auth } from "@/lib/auth";
import { db, WebsiteLoginLogs } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{ signedOutAll?: string }>;
+3 -1
View File
@@ -9,7 +9,9 @@ import { db, WebsiteShopArticles, WebsiteShopCategories } from "@/lib/db";
import { excerpt } from "@/lib/format";
import { creditsPerUnit } from "@/lib/services/paypal";
export const revalidate = 300;
// 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;
export const metadata = { title: "Shop" };
+3 -1
View File
@@ -11,7 +11,9 @@ import {
} from "@/lib/services/paypal";
import TopUpForm from "./TopUpForm";
export const dynamic = "force-dynamic";
// 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;
export default async function TopUpPage({
searchParams,
+3 -1
View File
@@ -5,7 +5,9 @@ import { ContentCard, EmptyState } from "@/components/public/ui";
import { db, User, WebsiteTeams } from "@/lib/db";
import { siteSettings } from "@/lib/services/site-settings";
export const revalidate = 300;
// 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;
export const metadata = { title: "Staff" };
+3 -1
View File
@@ -28,7 +28,9 @@ import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date";
import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
// 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 SearchParams = Promise<{
friend?: string;
+3 -1
View File
@@ -5,7 +5,9 @@ import { getTranslations } from "next-intl/server";
import { isValidVerificationToken } from "@/actions/email-verify";
import { db, User } from "@/lib/db";
export const dynamic = "force-dynamic";
// 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 Status = "verified" | "already" | "invalid" | "unavailable";
+4
View File
@@ -5,6 +5,10 @@ import { StatusCard } from "@/components/admin/dashboard";
import { Achievements, db } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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 Achievement = {
name: string;
category: string;
+4
View File
@@ -9,6 +9,10 @@ import { db, WebsiteAds } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { EditAdDeleteButton } from "../edit-ad-delete-button";
// 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;
export default async function EditAd({
params,
}: {
+4
View File
@@ -5,6 +5,10 @@ import { createAd } from "@/actions/admin-ads";
import { Button } from "@/components/ui/button";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function NewAd() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_EDIT, session.user.rank)) {
+4
View File
@@ -9,6 +9,10 @@ import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { AdsTable } from "./ads-table";
// 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;
export default async function AdminAds() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
+4
View File
@@ -8,6 +8,10 @@ import { AlertLogs, db } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
const CRITICAL = new Set(["critical", "error", "danger"]);
const WARNING = new Set(["warning", "warn"]);
@@ -8,6 +8,10 @@ import { Ban, db, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { redisCache } from "@/lib/redis-cache";
// 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 DayRow = {
label: string;
chats: number;
+4
View File
@@ -16,6 +16,10 @@ import {
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { redisCache } from "@/lib/redis-cache";
// 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 EconomyData = {
totalCredits: number;
totalPixels: number;
+4
View File
@@ -17,6 +17,10 @@ import { Ban, db, Rooms, RoomTradeLog, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { redisCache } from "@/lib/redis-cache";
// 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 AnalyticsData = {
totalUsers: number;
onlineUsers: number;
+4
View File
@@ -9,6 +9,10 @@ import { db, User, WebsiteStaffApplications } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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 ApplicationRow = {
id: bigint;
userId: number;
+4
View File
@@ -9,6 +9,10 @@ import { Button } from "@/components/ui/button";
import { db, WebsiteArticles } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function EditArticle({
params,
searchParams,
+4
View File
@@ -6,6 +6,10 @@ import { createArticle } from "@/actions/admin-articles";
import { ArticleForm } from "@/components/admin/article-form";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function NewArticle({
searchParams,
}: {
+4
View File
@@ -11,6 +11,10 @@ import { db, User, WebsiteArticles } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function AdminArticles({
searchParams,
}: {
+4
View File
@@ -7,6 +7,10 @@ import { Button } from "@/components/ui/button";
import { db, WebsiteBadges } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export const metadata = { title: "Badges" };
export default async function AdminBadges({
+4
View File
@@ -4,6 +4,10 @@ import { db, WebsiteBanner } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { BannersManager } from "./banners-manager";
// 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;
export default async function BannersPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.BANNERS_VIEW, session.user.rank)) {
+4
View File
@@ -11,6 +11,10 @@ import { Ban, db } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function AdminBans() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.BANS_VIEW, session.user.rank)) {
+4
View File
@@ -6,6 +6,10 @@ import { CalendarCampaigns, CalendarRewards, db } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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 Campaign = {
id: number;
name: string;
+4
View File
@@ -7,6 +7,10 @@ import { CalendarCampaigns, CalendarRewards, db } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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 Campaign = {
id: number;
name: string;
+4
View File
@@ -9,6 +9,10 @@ import { loadCatalogItemsData } from "@/lib/services/catalog-items-loader";
import { getAncestors } from "@/lib/services/catalog-tree";
import { CatalogDetailTabs } from "./catalog-detail-tabs";
// 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;
export default async function CatalogEditPage({
params,
}: {
@@ -4,6 +4,10 @@ import { CatalogItemsBc, CatalogPagesBc, db } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { BcPageDetail } from "../bc-manager";
// 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;
export default async function BcPageDetailPage({
params: paramsPromise,
}: {
@@ -1,5 +1,9 @@
import { redirect } from "next/navigation";
// 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;
export default function BuilderClubPage() {
redirect("/admin/catalog?catalog=bc");
}
+4
View File
@@ -31,6 +31,10 @@ import { CatalogManagerDialog } from "./catalog-manager-dialog-wrapper";
import { CatalogTable } from "./catalog-table";
import { CatalogViewToggle } from "./catalog-view-toggle";
// 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;
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
return rows ?? [];
+4
View File
@@ -18,6 +18,10 @@ import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings";
import { CommandocentrumControls } from "./commandocentrum-controls";
// 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;
function decodeStacktrace(raw: unknown): string {
if (raw == null) return "";
try {
+4
View File
@@ -5,6 +5,10 @@ import { Card, CardContent } from "@/components/ui/card";
import { db, EmulatorErrors } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function EmulatorErrorsPage({
searchParams,
}: {
+4
View File
@@ -10,6 +10,10 @@ import { db, EmulatorErrors, EmulatorSettings } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { HealthCheckClient } from "./health-check-client";
// 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;
async function getDevOpsData() {
const now = Math.floor(Date.now() / 1000);
const dayAgo = now - 86400;
+4
View File
@@ -10,6 +10,10 @@ import { Button } from "@/components/ui/button";
import { db, EmailTemplates } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function AdminEmailTemplates() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
+4
View File
@@ -10,6 +10,10 @@ import { Button } from "@/components/ui/button";
import { db, EmulatorSettings, EmulatorTexts } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
const TEXTS_LIMIT = 300;
export default async function AdminEmulator() {
+4
View File
@@ -12,6 +12,10 @@ import { EventPrizesManager } from "./event-prizes";
import { EventRegistrations } from "./event-registrations";
import { EventWinnersManager } from "./event-winners";
// 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;
export default async function EventDetailPage({
params,
}: {
+4
View File
@@ -5,6 +5,10 @@ import { db, WebsiteEventType } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { EventForm } from "../event-form";
// 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;
export default async function CreateEventPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.EVENTS_EDIT, session.user.rank)) {
+4
View File
@@ -7,6 +7,10 @@ import { db, WebsiteEvent, WebsiteEventType } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { EventsTable } from "./events-table";
// 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;
export default async function EventsPage({
params: paramsPromise,
searchParams,
+4
View File
@@ -4,6 +4,10 @@ import { db, WebsiteEventType } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { EventTypesManager } from "./event-types-manager";
// 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;
export default async function EventTypesPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.EVENTS_EDIT, session.user.rank)) {
+4
View File
@@ -4,6 +4,10 @@ import { db, WebsiteSetting } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { FaviconForm } from "./favicon-form";
// 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;
export default async function AdminFaviconPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
+4
View File
@@ -8,6 +8,10 @@ import { db, Guilds, GuildsForumsThreads, GuildsMembers, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function AdminGuildDetailPage({
params,
}: {
+4
View File
@@ -12,6 +12,10 @@ import { db, Guilds, GuildsMembers, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function AdminGuildsPage({
searchParams,
}: {
@@ -7,6 +7,10 @@ import { Button } from "@/components/ui/button";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function EditHelpQuestion({
params,
searchParams,
@@ -5,6 +5,10 @@ import { createHelpQuestion } from "@/actions/admin-help";
import { Button } from "@/components/ui/button";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function NewHelpQuestion({
searchParams,
}: {
+4
View File
@@ -7,6 +7,10 @@ import { Button } from "@/components/ui/button";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
// 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;
export default async function AdminHelpQuestions() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.PAGES_VIEW, session.user.rank)) {
Loaded 100 of 254 files, more files were not shown because too many files have changed in this diff. Show more