Files
EpicNext-Cms/src/app/help/page.tsx
T
simoleo89 e96b606e1e Add remaining public + admin pages (parallel build, 26 files)
Built via two parallel agent workflows reading the real Prisma schema, then
integrated + verified.

Public: /shop (categories + storefront), /community hub, /rankings (top by
credits), /staff, /photos (camera_web gallery), /help (categories + rules),
/help/tickets (auth-gated user tickets + create), /settings (auth-gated, update
motto via RCON).

Admin: /admin/catalog (+[id] items), /admin/radio (shouts/apps/schedules +
delete shout), /admin/teams (CRUD), /admin/permissions (read), /admin/wordfilter
(CRUD + RCON push), /admin/ip (whitelist/blacklist CRUD), /admin/applications
(list + dismiss), /admin/logs (chat/command/alert read), /admin/achievements
(read). Server actions all staff-gated.

Header + admin nav extended. Verified: tsc exit 0, vitest 48/48, next build
exit 0 (33 routes); curl-probed every route — public 200/<main>, auth+admin
correctly 307-redirect when unauthorized.
2026-06-28 13:24:55 +02:00

209 lines
6.4 KiB
TypeScript

import Link from "next/link";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
type HelpCategory = {
id: bigint;
name: string;
content: string;
imageUrl: string | null;
buttonText: string | null;
buttonUrl: string | null;
buttonColor: string;
buttonBorderColor: string;
smallBox: boolean;
};
type RuleCategory = {
id: bigint;
name: string;
description: string;
badge: string;
};
type Rule = {
id: bigint;
categoryId: bigint | null;
paragraph: string;
rule: string;
};
function isExternal(url: string): boolean {
return /^https?:\/\//i.test(url);
}
export default async function HelpCenterPage() {
let categories: HelpCategory[] = [];
try {
categories = await prisma.websiteHelpCenterCategories.findMany({
orderBy: [{ position: "asc" }, { id: "asc" }],
select: {
id: true,
name: true,
content: true,
imageUrl: true,
buttonText: true,
buttonUrl: true,
buttonColor: true,
buttonBorderColor: true,
smallBox: true,
},
});
} catch {
// DB unavailable — render the shell without help categories.
}
let ruleCategories: RuleCategory[] = [];
try {
ruleCategories = await prisma.websiteRuleCategories.findMany({
orderBy: { id: "asc" },
select: { id: true, name: true, description: true, badge: true },
});
} catch {
// DB unavailable — render the shell without rule categories.
}
let rules: Rule[] = [];
try {
rules = await prisma.websiteRules.findMany({
orderBy: [{ categoryId: "asc" }, { paragraph: "asc" }, { id: "asc" }],
select: { id: true, categoryId: true, paragraph: true, rule: true },
});
} catch {
// DB unavailable — render the shell without rules.
}
// Group the rules under their categories.
const rulesByCategory = new Map<string, Rule[]>();
for (const r of rules) {
const key = r.categoryId === null ? "" : String(r.categoryId);
const list = rulesByCategory.get(key);
if (list) {
list.push(r);
} else {
rulesByCategory.set(key, [r]);
}
}
return (
<main>
<section className="hero">
<h1 style={{ marginTop: 0 }}>Help Center</h1>
<p className="muted" style={{ fontSize: 16, margin: 0 }}>
Find guides, support topics, and the hotel rules — everything you need to get help.
</p>
</section>
<h2>Support topics</h2>
{categories.length === 0 ? (
<p className="muted">No help topics yet.</p>
) : (
<div className="grid cols-2">
{categories.map((c) => (
<article key={String(c.id)} className="card hover">
<div style={{ display: "flex", gap: "0.85rem", alignItems: "flex-start" }}>
{c.imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={c.imageUrl}
alt=""
width={48}
height={48}
style={{ flexShrink: 0, objectFit: "contain" }}
/>
) : null}
<div style={{ flex: 1 }}>
<h3 style={{ margin: "0 0 0.35rem" }}>{c.name}</h3>
<p className="muted" style={{ margin: 0 }}>
{c.content}
</p>
{c.buttonUrl && c.buttonText ? (
<div style={{ marginTop: "0.85rem" }}>
{isExternal(c.buttonUrl) ? (
<a
className="btn btn-outline"
href={c.buttonUrl}
target="_blank"
rel="noreferrer"
style={{
background: c.buttonColor,
borderColor: c.buttonBorderColor,
color: "#3a2c05",
}}
>
{c.buttonText}
</a>
) : (
<Link
className="btn btn-outline"
href={c.buttonUrl}
style={{
background: c.buttonColor,
borderColor: c.buttonBorderColor,
color: "#3a2c05",
}}
>
{c.buttonText}
</Link>
)}
</div>
) : null}
</div>
</div>
</article>
))}
</div>
)}
<h2 style={{ marginTop: "2rem" }}>Hotel rules</h2>
{ruleCategories.length === 0 ? (
<p className="muted">No rules published yet.</p>
) : (
<div className="grid">
{ruleCategories.map((cat) => {
const catRules = rulesByCategory.get(String(cat.id)) ?? [];
return (
<article key={String(cat.id)} className="card">
<div
style={{
display: "flex",
alignItems: "baseline",
gap: "0.6rem",
flexWrap: "wrap",
}}
>
<h3 style={{ margin: 0 }}>{cat.name}</h3>
{cat.badge ? <span className="muted">{cat.badge}</span> : null}
</div>
{cat.description ? (
<p className="muted" style={{ margin: "0.35rem 0 0" }}>
{cat.description}
</p>
) : null}
{catRules.length === 0 ? (
<p className="muted" style={{ marginBottom: 0 }}>
No rules in this category.
</p>
) : (
<ul style={{ margin: "0.85rem 0 0", paddingLeft: "1.2rem" }}>
{catRules.map((r) => (
<li key={String(r.id)} style={{ marginBottom: "0.5rem" }}>
{r.paragraph ? (
<strong style={{ marginRight: "0.4rem" }}>{r.paragraph}</strong>
) : null}
{r.rule}
</li>
))}
</ul>
)}
</article>
);
})}
</div>
)}
</main>
);
}