import { and, desc, eq, inArray } from "drizzle-orm"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import type { CSSProperties } from "react"; import Link from "@/components/link"; import { PublicLoadError } from "@/components/public/load-error"; import { ContentCard, EmptyState } from "@/components/public/ui"; import { db, Guilds, GuildsForumsThreads, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { publicReadFailure } from "@/lib/services/public-read"; type SearchParams = Promise<{ posted?: 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}`, }; } type ThreadRow = { id: number; openerId: number | null; subject: string | null; postsCount: number | null; createdAt: number | null; updatedAt: number | null; pinned: number | null; locked: number | null; }; export default async function GuildForumPage({ params, searchParams, }: { params: Promise<{ id: string }>; searchParams: SearchParams; }) { const t = await getTranslations("pages.guildForum"); const { id } = await params; const { posted } = await searchParams; const guildId = Number(id); if (!Number.isInteger(guildId) || guildId <= 0) notFound(); // Missing guilds and unavailable guild data have distinct outcomes. let guild: { id: number; name: string; badge: string } | null = null; try { const [row] = await db .select({ id: Guilds.id, name: Guilds.name, badge: Guilds.badge, }) .from(Guilds) .where(eq(Guilds.id, guildId)) .limit(1); guild = row ?? null; } catch (error) { publicReadFailure("guild.forum")(error); return (
); } if (!guild) notFound(); // Threads for this guild. Pinned first, then most-recently active. let threadsFailed = false; let authorsFailed = false; let threads: ThreadRow[] = []; try { threads = await db .select({ id: GuildsForumsThreads.id, openerId: GuildsForumsThreads.openerId, subject: GuildsForumsThreads.subject, postsCount: GuildsForumsThreads.postsCount, createdAt: GuildsForumsThreads.createdAt, updatedAt: GuildsForumsThreads.updatedAt, pinned: GuildsForumsThreads.pinned, locked: GuildsForumsThreads.locked, }) .from(GuildsForumsThreads) .where( and( eq(GuildsForumsThreads.guildId, guild.id), eq(GuildsForumsThreads.state, 0), ), ) .orderBy( desc(GuildsForumsThreads.pinned), desc(GuildsForumsThreads.updatedAt), desc(GuildsForumsThreads.id), ) .limit(100); } catch (error) { publicReadFailure("guild.threads")(error); threadsFailed = true; threads = []; } // Resolve opener usernames in one query (no relation exists in the schema, so // join the users table manually by id). Falls back to the raw author id. const openerIds = Array.from( new Set(threads.map((t2) => t2.openerId ?? 0).filter((v) => v > 0)), ); let users: { id: number; username: string }[] = []; try { users = openerIds.length ? await db .select({ id: User.id, username: User.username }) .from(User) .where(inArray(User.id, openerIds)) : []; } catch (error) { publicReadFailure("guild.thread-authors")(error); authorsFailed = true; users = []; } const usernameById = new Map(users.map((u) => [u.id, u.username])); return (
{posted === "1" ? (
{t("success.posted")}
) : null}

{t("backToGuild")}

{t("newThread")} } /> {authorsFailed && ( )} {threadsFailed ? ( ) : threads.length === 0 ? ( {t("empty")} ) : ( {threads.map((t2) => { const opener = t2.openerId ?? 0; const username = opener > 0 ? usernameById.get(opener) : undefined; return ( ); })}
{t("colSubject")} {t("colAuthor")} {t("colPosts")} {t("colDate")}
{t2.subject?.trim() || t("noSubject")} {t2.pinned ? ( 📌 {t("pinned")} ) : null} {t2.locked ? ( 🔒 {t("locked")} ) : null} {username ? ( {username} ) : ( {opener > 0 ? t("userNumber", { id: opener }) : t("unknown")} )} {t2.postsCount ?? 0} {formatDate( t2.createdAt ? new Date(t2.createdAt * 1000) : null, "date", )}
)}
); }