import { asc, eq, inArray } from "drizzle-orm"; import { notFound } from "next/navigation"; import { getTranslations } from "next-intl/server"; import Link from "@/components/link"; import { PublicLoadError } from "@/components/public/load-error"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail"; import { db, Guilds, GuildsMembers, User } from "@/lib/db"; import { formatDate } from "@/lib/format-date"; import { publicReadFailure } from "@/lib/services/public-read"; type GuildView = { id: number; name: string; description: string; badge: string; userId: number; dateCreated: number; }; type MemberView = { userId: number; username: string; look: string; motto: string; memberSince: number; isOwner: boolean; }; export default async function GuildPage({ params, }: { params: Promise<{ id: string }>; }) { const t = await getTranslations("pages.guild"); const { id } = await params; const guildId = Number(id); if (!Number.isInteger(guildId) || guildId <= 0) notFound(); let guild: GuildView | null = null; try { const [row] = await db .select({ id: Guilds.id, name: Guilds.name, description: Guilds.description, badge: Guilds.badge, userId: Guilds.userId, dateCreated: Guilds.dateCreated, }) .from(Guilds) .where(eq(Guilds.id, guildId)) .limit(1); guild = row ?? null; } catch (error) { publicReadFailure("guild.detail")(error); return ( ); } if (!guild) notFound(); // Members come from guilds_members; there is no relation in the schema, so we // join the users table manually by id. let membersFailed = false; let memberRows: { userId: number; memberSince: number }[] = []; try { memberRows = await db .select({ userId: GuildsMembers.userId, memberSince: GuildsMembers.memberSince, }) .from(GuildsMembers) .where(eq(GuildsMembers.guildId, guild.id)) .orderBy(asc(GuildsMembers.memberSince)) .limit(200); } catch (error) { publicReadFailure("guild.members")(error); membersFailed = true; memberRows = []; } const memberIds = Array.from( new Set( [guild.userId, ...memberRows.map((m) => m.userId)].filter((v) => v > 0), ), ); let users: { id: number; username: string; look: string; motto: string }[] = []; try { users = memberIds.length ? await db .select({ id: User.id, username: User.username, look: User.look, motto: User.motto, }) .from(User) .where(inArray(User.id, memberIds)) : []; } catch (error) { publicReadFailure("guild.member-users")(error); membersFailed = true; users = []; } const usersById = new Map(users.map((u) => [u.id, u])); const sinceById = new Map(memberRows.map((m) => [m.userId, m.memberSince])); const guildOwnerUserId = guild.userId; const members: MemberView[] = memberIds .map((uid) => { const u = usersById.get(uid); if (!u) return null; return { userId: u.id, username: u.username, look: u.look, motto: u.motto, memberSince: sinceById.get(uid) ?? 0, isOwner: uid === guildOwnerUserId, }; }) .filter((m): m is MemberView => m !== null) // Owner first, then by join date. .sort((a, b) => { if (a.isOwner !== b.isOwner) return a.isOwner ? -1 : 1; return a.memberSince - b.memberSince; }); const created = formatDate(new Date(guild.dateCreated * 1000), "date"); return ( {t("allGuilds")} {guild.badge ? ( // eslint-disable-next-line @next/next/no-img-element ) : null} {guild.name || t("unnamedGuild")} {guild.description || ( {t("noDescription")} )} {created ? ( ) : null} {t("forumLink")} } label={t("discussion")} /> {membersFailed && } {members.length === 0 ? ( membersFailed ? null : ( {t("noMembers")} ) ) : ( {members.map((m) => ( {m.username} {m.isOwner ? ( 👑 {t("owner")} ) : null} {m.motto || t("noMotto")} ))} )} ); }
{t("allGuilds")}
{guild.description || ( {t("noDescription")} )}
{m.motto || t("noMotto")}