style: format code biome

This commit is contained in:
openhands committed 2026-07-13 21:57:41 +02:00
1 parent 8efd032cc6
commit df38dccbf1
735 files changed
+128321 -120870

No files matched your search

+129 -118
View File
@@ -1,6 +1,6 @@
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { ContentCard } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
import { sanitize } from "@/lib/sanitize";
@@ -12,142 +12,153 @@ export const revalidate = 300;
const HELP_IMAGE_BASE = "/assets/images/help-center";
type HelpCategory = {
id: bigint;
name: string;
content: string;
position: number;
imageUrl: string | null;
buttonText: string | null;
buttonUrl: string | null;
buttonColor: string;
buttonBorderColor: string;
smallBox: boolean;
id: bigint;
name: string;
content: string;
position: number;
imageUrl: string | null;
buttonText: string | null;
buttonUrl: string | null;
buttonColor: string;
buttonBorderColor: string;
smallBox: boolean;
};
type HelpCategoryLink = {
id: bigint;
name: string;
position: number;
id: bigint;
name: string;
position: number;
};
// Faithful to AtomCMS: name / content / button_text carry a `:hotel` placeholder
// replaced with the configured hotel name before display.
function withHotel(text: string | null | undefined, hotelName: string): string {
return (text ?? "").split(":hotel").join(hotelName);
return (text ?? "").split(":hotel").join(hotelName);
}
export default async function HelpCategoryPage({ params }: { params: Promise<{ category: string }> }) {
const t = await getTranslations("pages.helpCategory");
const { category } = await params;
export default async function HelpCategoryPage({
params,
}: {
params: Promise<{ category: string }>;
}) {
const t = await getTranslations("pages.helpCategory");
const { category } = await params;
let categoryId: bigint;
try {
categoryId = BigInt(category);
} catch {
notFound();
}
let categoryId: bigint;
try {
categoryId = BigInt(category);
} catch {
notFound();
}
let cat: HelpCategory | null = null;
try {
cat = await prisma.websiteHelpCenterCategories.findUnique({
where: { id: categoryId },
select: {
id: true,
name: true,
content: true,
position: true,
imageUrl: true,
buttonText: true,
buttonUrl: true,
buttonColor: true,
buttonBorderColor: true,
smallBox: true,
},
});
} catch {
cat = null;
}
let cat: HelpCategory | null = null;
try {
cat = await prisma.websiteHelpCenterCategories.findUnique({
where: { id: categoryId },
select: {
id: true,
name: true,
content: true,
position: true,
imageUrl: true,
buttonText: true,
buttonUrl: true,
buttonColor: true,
buttonBorderColor: true,
smallBox: true,
},
});
} catch {
cat = null;
}
if (!cat) notFound();
if (!cat) notFound();
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
const hotelName = (await siteSettings.get("hotel_name", "Atom")) ?? "Atom";
// Sibling help topics, so the reader can jump between categories. Isolated
// query: a DB hiccup degrades this list to empty rather than 500-ing.
let siblings: HelpCategoryLink[] = [];
try {
siblings = await prisma.websiteHelpCenterCategories.findMany({
where: { id: { not: categoryId } },
orderBy: { position: "asc" },
select: { id: true, name: true, position: true },
take: 50,
});
} catch {
siblings = [];
}
// Sibling help topics, so the reader can jump between categories. Isolated
// query: a DB hiccup degrades this list to empty rather than 500-ing.
let siblings: HelpCategoryLink[] = [];
try {
siblings = await prisma.websiteHelpCenterCategories.findMany({
where: { id: { not: categoryId } },
orderBy: { position: "asc" },
select: { id: true, name: true, position: true },
take: 50,
});
} catch {
siblings = [];
}
const title = withHotel(cat.name, hotelName);
const content = withHotel(cat.content, hotelName);
const buttonText = withHotel(cat.buttonText, hotelName);
const imageSrc = cat.imageUrl ? `${HELP_IMAGE_BASE}/${cat.imageUrl}` : "";
const hasButton = Boolean(cat.buttonText && cat.buttonText.trim() !== "");
const title = withHotel(cat.name, hotelName);
const content = withHotel(cat.content, hotelName);
const buttonText = withHotel(cat.buttonText, hotelName);
const imageSrc = cat.imageUrl ? `${HELP_IMAGE_BASE}/${cat.imageUrl}` : "";
const hasButton = Boolean(cat.buttonText && cat.buttonText.trim() !== "");
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<p style={{ margin: 0 }}>
<Link href="/help">{t("back")}</Link>
</p>
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<p style={{ margin: 0 }}>
<Link href="/help">{t("back")}</Link>
</p>
<ContentCard icon="❓" title={title} subtitle={t("topicSubtitle")}>
{imageSrc ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imageSrc}
alt=""
style={{
float: "right",
maxWidth: 160,
height: "auto",
margin: "0 0 0.75rem 1rem",
}}
/>
) : null}
<ContentCard icon="❓" title={title} subtitle={t("topicSubtitle")}>
{imageSrc ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imageSrc}
alt=""
style={{
float: "right",
maxWidth: 160,
height: "auto",
margin: "0 0 0.75rem 1rem",
}}
/>
) : null}
{/* content is author-supplied HTML — sanitised server-side. */}
<div style={{ lineHeight: 1.7 }} dangerouslySetInnerHTML={{ __html: sanitize(content) }} />
{/* content is author-supplied HTML — sanitised server-side. */}
<div
style={{ lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: sanitize(content) }}
/>
{hasButton ? (
<div style={{ clear: "both", marginTop: "1rem" }}>
<a
href={cat.buttonUrl ?? "#"}
className="btn"
style={{
backgroundColor: cat.buttonColor,
border: `2px solid ${cat.buttonBorderColor}`,
}}
>
{buttonText}
</a>
</div>
) : null}
</ContentCard>
{hasButton ? (
<div style={{ clear: "both", marginTop: "1rem" }}>
<a
href={cat.buttonUrl ?? "#"}
className="btn"
style={{
backgroundColor: cat.buttonColor,
border: `2px solid ${cat.buttonBorderColor}`,
}}
>
{buttonText}
</a>
</div>
) : null}
</ContentCard>
{siblings.length > 0 ? (
<ContentCard icon="🧭" title={t("moreTitle")} subtitle={t("moreSubtitle")}>
<div className="card-grid sm-2 lg-3">
{siblings.map((s) => (
<Link
key={String(s.id)}
href={`/help/${String(s.id)}`}
className="card hover"
style={{ display: "block", color: "inherit" }}
>
<strong>{withHotel(s.name, hotelName)}</strong>
</Link>
))}
</div>
</ContentCard>
) : null}
</main>
);
{siblings.length > 0 ? (
<ContentCard
icon="🧭"
title={t("moreTitle")}
subtitle={t("moreSubtitle")}
>
<div className="card-grid sm-2 lg-3">
{siblings.map((s) => (
<Link
key={String(s.id)}
href={`/help/${String(s.id)}`}
className="card hover"
style={{ display: "block", color: "inherit" }}
>
<strong>{withHotel(s.name, hotelName)}</strong>
</Link>
))}
</div>
</ContentCard>
) : null}
</main>
);
}
+219 -200
View File
@@ -1,228 +1,247 @@
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
export const revalidate = 300;
type HelpCategory = {
id: bigint;
name: string;
content: string;
imageUrl: string | null;
buttonText: string | null;
buttonUrl: string | null;
buttonColor: string;
buttonBorderColor: string;
smallBox: boolean;
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;
id: bigint;
name: string;
description: string;
badge: string;
};
type Rule = {
id: bigint;
categoryId: bigint | null;
paragraph: string;
rule: string;
id: bigint;
categoryId: bigint | null;
paragraph: string;
rule: string;
};
function isExternal(url: string): boolean {
return /^https?:\/\//i.test(url);
return /^https?:\/\//i.test(url);
}
export default async function HelpCenterPage() {
const t = await getTranslations("pages.help");
const t = await getTranslations("pages.help");
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 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 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.
}
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]);
}
}
// 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 style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard
icon="❓"
title={t("title")}
subtitle={t("subtitle")}
action={
<Link className="btn btn-outline" href="/help/tickets">
{t("myTickets")}
</Link>
}
/>
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard
icon="❓"
title={t("title")}
subtitle={t("subtitle")}
action={
<Link className="btn btn-outline" href="/help/tickets">
{t("myTickets")}
</Link>
}
/>
<ContentCard
icon="🧭"
title={t("topicsTitle")}
subtitle={t("topicsSubtitle")}
padded={categories.length === 0}
>
{categories.length === 0 ? (
<EmptyState icon="🧭">{t("topicsEmpty")}</EmptyState>
) : (
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
{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, minWidth: 0 }}>
<h3 style={{ margin: "0 0 0.35rem", fontSize: "1rem" }}>{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>
)}
</ContentCard>
<ContentCard
icon="🧭"
title={t("topicsTitle")}
subtitle={t("topicsSubtitle")}
padded={categories.length === 0}
>
{categories.length === 0 ? (
<EmptyState icon="🧭">{t("topicsEmpty")}</EmptyState>
) : (
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
{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, minWidth: 0 }}>
<h3 style={{ margin: "0 0 0.35rem", fontSize: "1rem" }}>
{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>
)}
</ContentCard>
<ContentCard
icon="📜"
title={t("rulesTitle")}
subtitle={t("rulesSubtitle")}
padded={ruleCategories.length === 0}
>
{ruleCategories.length === 0 ? (
<EmptyState icon="📜">{t("rulesEmpty")}</EmptyState>
) : (
<div style={{ display: "grid", gap: "1rem", padding: "1rem" }}>
{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, fontSize: "1.05rem" }}>{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}
<ContentCard
icon="📜"
title={t("rulesTitle")}
subtitle={t("rulesSubtitle")}
padded={ruleCategories.length === 0}
>
{ruleCategories.length === 0 ? (
<EmptyState icon="📜">{t("rulesEmpty")}</EmptyState>
) : (
<div style={{ display: "grid", gap: "1rem", padding: "1rem" }}>
{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, fontSize: "1.05rem" }}>
{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 }}>
{t("categoryEmpty")}
</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>
)}
</ContentCard>
</main>
);
{catRules.length === 0 ? (
<p className="muted" style={{ marginBottom: 0 }}>
{t("categoryEmpty")}
</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>
)}
</ContentCard>
</main>
);
}
+93 -84
View File
@@ -1,5 +1,5 @@
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { createTicket } from "@/actions/help-tickets";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { auth } from "@/lib/auth";
@@ -8,97 +8,106 @@ import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
function formatDate(d: Date | null): string {
return d ? d.toISOString().slice(0, 16).replace("T", " ") : "";
return d ? d.toISOString().slice(0, 16).replace("T", " ") : "";
}
export default async function HelpTicketsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const session = await auth();
if (!session?.user?.id) redirect("/login");
const t = await getTranslations("pages.helpTickets");
const t = await getTranslations("pages.helpTickets");
const userId = Number(session.user.id);
const userId = Number(session.user.id);
let tickets: Awaited<ReturnType<typeof prisma.websiteHelpCenterTickets.findMany>> = [];
try {
tickets = await prisma.websiteHelpCenterTickets.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
take: 100,
});
} catch {
tickets = [];
}
let tickets: Awaited<
ReturnType<typeof prisma.websiteHelpCenterTickets.findMany>
> = [];
try {
tickets = await prisma.websiteHelpCenterTickets.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
take: 100,
});
} catch {
tickets = [];
}
const openCount = tickets.filter((t) => t.open).length;
const closedCount = tickets.length - openCount;
const openCount = tickets.filter((t) => t.open).length;
const closedCount = tickets.length - openCount;
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard icon="🎫" title={t("title")} subtitle={t("subtitle")}>
<div className="stat-grid">
<StatBlock icon="🎫" value={tickets.length} label={t("statTotal")} />
<StatBlock icon="📬" value={openCount} label={t("statOpen")} />
<StatBlock icon="✅" value={closedCount} label={t("statClosed")} />
</div>
</ContentCard>
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard icon="🎫" title={t("title")} subtitle={t("subtitle")}>
<div className="stat-grid">
<StatBlock icon="🎫" value={tickets.length} label={t("statTotal")} />
<StatBlock icon="📬" value={openCount} label={t("statOpen")} />
<StatBlock icon="✅" value={closedCount} label={t("statClosed")} />
</div>
</ContentCard>
<ContentCard icon="✏️" title={t("newTitle")} subtitle={t("newSubtitle")}>
<form action={createTicket}>
<div style={{ display: "flex", flexDirection: "column", gap: "0.6rem" }}>
<input name="title" placeholder={t("subjectPlaceholder")} maxLength={255} required />
<textarea
name="content"
placeholder={t("contentPlaceholder")}
rows={5}
maxLength={5000}
required
/>
<div>
<button type="submit" className="btn btn-primary">
{t("submit")}
</button>
</div>
</div>
</form>
</ContentCard>
<ContentCard icon="✏️" title={t("newTitle")} subtitle={t("newSubtitle")}>
<form action={createTicket}>
<div
style={{ display: "flex", flexDirection: "column", gap: "0.6rem" }}
>
<input
name="title"
placeholder={t("subjectPlaceholder")}
maxLength={255}
required
/>
<textarea
name="content"
placeholder={t("contentPlaceholder")}
rows={5}
maxLength={5000}
required
/>
<div>
<button type="submit" className="btn btn-primary">
{t("submit")}
</button>
</div>
</div>
</form>
</ContentCard>
<ContentCard
icon="📋"
title={t("listTitle")}
subtitle={t("listSubtitle")}
padded={tickets.length === 0}
>
{tickets.length === 0 ? (
<EmptyState icon="🎫">{t("listEmpty")}</EmptyState>
) : (
<table>
<thead>
<tr>
<th>{t("colId")}</th>
<th>{t("colSubject")}</th>
<th>{t("colStatus")}</th>
<th>{t("colCreated")}</th>
</tr>
</thead>
<tbody>
{tickets.map((t2) => (
<tr key={String(t2.id)}>
<td>{String(t2.id)}</td>
<td>{t2.title}</td>
<td>
<span className={`online-badge${t2.open ? " online" : ""}`}>
<span className="online-dot" aria-hidden />
{t2.open ? t("statusOpen") : t("statusClosed")}
</span>
</td>
<td className="muted">{formatDate(t2.createdAt)}</td>
</tr>
))}
</tbody>
</table>
)}
</ContentCard>
</main>
);
<ContentCard
icon="📋"
title={t("listTitle")}
subtitle={t("listSubtitle")}
padded={tickets.length === 0}
>
{tickets.length === 0 ? (
<EmptyState icon="🎫">{t("listEmpty")}</EmptyState>
) : (
<table>
<thead>
<tr>
<th>{t("colId")}</th>
<th>{t("colSubject")}</th>
<th>{t("colStatus")}</th>
<th>{t("colCreated")}</th>
</tr>
</thead>
<tbody>
{tickets.map((t2) => (
<tr key={String(t2.id)}>
<td>{String(t2.id)}</td>
<td>{t2.title}</td>
<td>
<span className={`online-badge${t2.open ? " online" : ""}`}>
<span className="online-dot" aria-hidden />
{t2.open ? t("statusOpen") : t("statusClosed")}
</span>
</td>
<td className="muted">{formatDate(t2.createdAt)}</td>
</tr>
))}
</tbody>
</table>
)}
</ContentCard>
</main>
);
}