Add missing admin action files and navigation links

- Add 11 missing server action files: badges, bulk-users, catalog, catalog-bc, catalog-items, import-badges, import-furni, multi-account-detect, permissions, rooms, soundtracks
- Add missing admin navigation links: tickets, sounds, translations, import, radio sub-pages
- Add translation keys for all new navigation items
This commit is contained in:
openhands committed 2026-07-11 12:01:05 +02:00
1 parent 7e1ae17a3b
commit 41be6835bf
347 files changed
+28378 -6091

No files matched your search

+23 -28
View File
@@ -12,11 +12,7 @@ import { toggleReaction } from "@/actions/article-reactions";
export const dynamic = "force-dynamic";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
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 prisma.websiteArticles
@@ -49,18 +45,25 @@ function formatDate(d: Date | null | undefined): string {
return d ? d.toISOString().slice(0, 10) : "";
}
export default async function ArticlePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const t = await getTranslations("pages.article");
const article = await prisma.websiteArticles.findUnique({
where: { slug },
select: { id: true, slug: true, title: true, shortStory: true, fullStory: true, image: true, createdAt: true, updatedAt: true },
}).catch(() => null);
const article = await prisma.websiteArticles
.findUnique({
where: { slug },
select: {
id: true,
slug: true,
title: true,
shortStory: true,
fullStory: true,
image: true,
createdAt: true,
updatedAt: true,
},
})
.catch(() => null);
if (!article) notFound();
const articleId = article.id;
@@ -128,11 +131,7 @@ export default async function ArticlePage({
>
{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" }}
/>
<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. */}
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: sanitised article body */}
@@ -158,10 +157,7 @@ export default async function ArticlePage({
title={active ? t("removeReaction") : t("reactWith", { reaction: r.key })}
>
<span>{label}</span>
<span
className="muted"
style={{ fontSize: "0.85rem", color: "inherit", opacity: 0.85 }}
>
<span className="muted" style={{ fontSize: "0.85rem", color: "inherit", opacity: 0.85 }}>
{count}
</span>
</button>
@@ -196,7 +192,9 @@ export default async function ArticlePage({
<ContentCard
icon="🗨️"
title={t("commentsTitle")}
subtitle={comments.length > 0 ? t("commentsCount", { count: comments.length }) : t("commentsSubtitle")}
subtitle={
comments.length > 0 ? t("commentsCount", { count: comments.length }) : t("commentsSubtitle")
}
>
{comments.length === 0 ? (
<EmptyState icon="🗨️">{t("noComments")}</EmptyState>
@@ -224,10 +222,7 @@ export default async function ArticlePage({
{loggedIn ? (
<form action={postComment} className="card">
<input type="hidden" name="articleId" value={String(articleId)} />
<label
htmlFor="comment"
style={{ display: "block", fontWeight: 700, marginBottom: "0.5rem" }}
>
<label htmlFor="comment" style={{ display: "block", fontWeight: 700, marginBottom: "0.5rem" }}>
{t("addComment")}
</label>
<textarea
+4 -1
View File
@@ -1,7 +1,10 @@
export default function NewsLoading() {
return (
<div style={{ display: "flex", justifyContent: "center", padding: "4rem 0" }}>
<div className="animate-spin w-8 h-8 border-4 rounded-full" style={{ borderColor: "var(--color-primary)", borderTopColor: "transparent" }} />
<div
className="animate-spin w-8 h-8 border-4 rounded-full"
style={{ borderColor: "var(--color-primary)", borderTopColor: "transparent" }}
/>
</div>
);
}
+23 -15
View File
@@ -29,11 +29,7 @@ export default async function NewsPage() {
return (
<main style={{ display: "grid", gap: "1.5rem" }}>
<ContentCard
icon="📰"
title={t("title")}
subtitle={t("subtitle")}
/>
<ContentCard icon="📰" title={t("title")} subtitle={t("subtitle")} />
<ContentCard padded={articles.length === 0}>
{articles.length === 0 ? (
@@ -55,27 +51,39 @@ export default async function NewsPage() {
>
{a.image ? (
<div style={{ position: "relative", width: "100%", aspectRatio: "16/9" }}>
<Image
src={a.image}
alt=""
fill
style={{ objectFit: "cover" }}
unoptimized
/>
<Image src={a.image} alt="" fill style={{ objectFit: "cover" }} unoptimized />
</div>
) : (
<div
style={{ width: "100%", aspectRatio: "16/9", backgroundColor: "color-mix(in srgb, var(--color-primary) 10%, var(--color-navbar))" }}
style={{
width: "100%",
aspectRatio: "16/9",
backgroundColor: "color-mix(in srgb, var(--color-primary) 10%, var(--color-navbar))",
}}
/>
)}
<div style={{ padding: "1rem 1.1rem", flex: 1, display: "flex", flexDirection: "column" }}>
<h3 style={{ margin: 0, fontSize: "1.05rem", color: "var(--color-navbar-text)", lineHeight: 1.4 }}>
<h3
style={{
margin: 0,
fontSize: "1.05rem",
color: "var(--color-navbar-text)",
lineHeight: 1.4,
}}
>
{a.title}
</h3>
<p className="muted" style={{ margin: "0.35rem 0 0.6rem", fontSize: "0.78rem" }}>
{formatDate(a.createdAt)}
</p>
<p style={{ margin: 0, color: "var(--color-navbar-text)", fontSize: "0.88rem", lineHeight: 1.55 }}>
<p
style={{
margin: 0,
color: "var(--color-navbar-text)",
fontSize: "0.88rem",
lineHeight: 1.55,
}}
>
{excerpt(a.shortStory, 150)}
</p>
</div>