fix(public): distinguish unavailable data from empty results and missing pages
This commit is contained in:
1 parent
db4acbb46e
commit
445ef13846
16 files changed
+608
-169
No files matched your search
@@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server";
|
||||
import type { CSSProperties } from "react";
|
||||
import { replyToThread } from "@/actions/social";
|
||||
import Link from "@/components/link";
|
||||
import { PublicLoadError } from "@/components/public/load-error";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { auth } from "@/lib/auth";
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
User,
|
||||
} from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { publicReadFailure } from "@/lib/services/public-read";
|
||||
|
||||
type SearchParams = Promise<{ replied?: string; error?: string }>;
|
||||
|
||||
@@ -60,6 +62,8 @@ export default async function GuildForumThreadPage({
|
||||
pinned: number | null;
|
||||
openerId: number | null;
|
||||
} | null = null;
|
||||
let postsFailed = false;
|
||||
let authorsFailed = false;
|
||||
let posts: {
|
||||
id: number;
|
||||
userId: number;
|
||||
@@ -96,9 +100,15 @@ export default async function GuildForumThreadPage({
|
||||
]);
|
||||
guild = guildRow;
|
||||
thread = threadRow;
|
||||
} catch {
|
||||
guild = null;
|
||||
thread = null;
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.thread")(error);
|
||||
return (
|
||||
<main className="page-grid">
|
||||
<ContentCard>
|
||||
<PublicLoadError href={`/guilds/${guildId}/forum/${threadId}`} />
|
||||
</ContentCard>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!guild || !thread) notFound();
|
||||
@@ -122,7 +132,9 @@ export default async function GuildForumThreadPage({
|
||||
asc(GuildsForumsComments.createdAt),
|
||||
asc(GuildsForumsComments.id),
|
||||
);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.posts")(error);
|
||||
postsFailed = true;
|
||||
posts = [];
|
||||
}
|
||||
|
||||
@@ -137,7 +149,9 @@ export default async function GuildForumThreadPage({
|
||||
.from(User)
|
||||
.where(inArray(User.id, authorIds))
|
||||
: [];
|
||||
} catch {
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.post-authors")(error);
|
||||
authorsFailed = true;
|
||||
users = [];
|
||||
}
|
||||
const usernameById = new Map(users.map((u) => [u.id, u.username]));
|
||||
@@ -195,10 +209,17 @@ export default async function GuildForumThreadPage({
|
||||
<ContentCard
|
||||
icon="📝"
|
||||
title={t("postsTitle")}
|
||||
subtitle={t("postsSubtitle", { count: posts.length })}
|
||||
padded={posts.length === 0}
|
||||
subtitle={
|
||||
postsFailed ? undefined : t("postsSubtitle", { count: posts.length })
|
||||
}
|
||||
padded={posts.length === 0 || authorsFailed}
|
||||
>
|
||||
{posts.length === 0 ? (
|
||||
{authorsFailed && (
|
||||
<PublicLoadError href={`/guilds/${guild.id}/forum/${thread.id}`} />
|
||||
)}
|
||||
{postsFailed ? (
|
||||
<PublicLoadError href={`/guilds/${guild.id}/forum/${thread.id}`} />
|
||||
) : posts.length === 0 ? (
|
||||
<EmptyState icon="📝">{t("postsEmpty")}</EmptyState>
|
||||
) : (
|
||||
<ul
|
||||
|
||||
@@ -3,9 +3,11 @@ 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 }>;
|
||||
|
||||
@@ -49,7 +51,7 @@ export default async function GuildForumPage({
|
||||
const guildId = Number(id);
|
||||
if (!Number.isInteger(guildId) || guildId <= 0) notFound();
|
||||
|
||||
// Guild header (read-only). A DB hiccup degrades to notFound rather than 500.
|
||||
// Missing guilds and unavailable guild data have distinct outcomes.
|
||||
let guild: { id: number; name: string; badge: string } | null = null;
|
||||
try {
|
||||
const [row] = await db
|
||||
@@ -62,13 +64,22 @@ export default async function GuildForumPage({
|
||||
.where(eq(Guilds.id, guildId))
|
||||
.limit(1);
|
||||
guild = row ?? null;
|
||||
} catch {
|
||||
guild = null;
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.forum")(error);
|
||||
return (
|
||||
<main className="page-grid">
|
||||
<ContentCard>
|
||||
<PublicLoadError href={`/guilds/${guildId}/forum`} />
|
||||
</ContentCard>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -95,7 +106,9 @@ export default async function GuildForumPage({
|
||||
desc(GuildsForumsThreads.id),
|
||||
)
|
||||
.limit(100);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.threads")(error);
|
||||
threadsFailed = true;
|
||||
threads = [];
|
||||
}
|
||||
|
||||
@@ -113,7 +126,9 @@ export default async function GuildForumPage({
|
||||
.from(User)
|
||||
.where(inArray(User.id, openerIds))
|
||||
: [];
|
||||
} catch {
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.thread-authors")(error);
|
||||
authorsFailed = true;
|
||||
users = [];
|
||||
}
|
||||
|
||||
@@ -134,7 +149,11 @@ export default async function GuildForumPage({
|
||||
<ContentCard
|
||||
icon="🗣️"
|
||||
title={t("forumTitle", { guild: guild.name || t("unnamedGuild") })}
|
||||
subtitle={`${threads.length} ${threads.length === 1 ? t("threadSingular") : t("threadPlural")}`}
|
||||
subtitle={
|
||||
threadsFailed
|
||||
? undefined
|
||||
: `${threads.length} ${threads.length === 1 ? t("threadSingular") : t("threadPlural")}`
|
||||
}
|
||||
action={
|
||||
<Link
|
||||
href={`/guilds/${guild.id}/forum/new`}
|
||||
@@ -145,8 +164,13 @@ export default async function GuildForumPage({
|
||||
}
|
||||
/>
|
||||
|
||||
<ContentCard padded={threads.length === 0}>
|
||||
{threads.length === 0 ? (
|
||||
<ContentCard padded={threads.length === 0 || authorsFailed}>
|
||||
{authorsFailed && (
|
||||
<PublicLoadError href={`/guilds/${guild.id}/forum`} />
|
||||
)}
|
||||
{threadsFailed ? (
|
||||
<PublicLoadError href={`/guilds/${guild.id}/forum`} />
|
||||
) : threads.length === 0 ? (
|
||||
<EmptyState icon="🗣️">{t("empty")}</EmptyState>
|
||||
) : (
|
||||
<table>
|
||||
|
||||
@@ -2,10 +2,12 @@ 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;
|
||||
@@ -50,14 +52,22 @@ export default async function GuildPage({
|
||||
.where(eq(Guilds.id, guildId))
|
||||
.limit(1);
|
||||
guild = row ?? null;
|
||||
} catch {
|
||||
guild = null;
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.detail")(error);
|
||||
return (
|
||||
<main className="page-grid">
|
||||
<ContentCard>
|
||||
<PublicLoadError href={`/guilds/${guildId}`} />
|
||||
</ContentCard>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -69,7 +79,9 @@ export default async function GuildPage({
|
||||
.where(eq(GuildsMembers.guildId, guild.id))
|
||||
.orderBy(asc(GuildsMembers.memberSince))
|
||||
.limit(200);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.members")(error);
|
||||
membersFailed = true;
|
||||
memberRows = [];
|
||||
}
|
||||
|
||||
@@ -93,7 +105,9 @@ export default async function GuildPage({
|
||||
.from(User)
|
||||
.where(inArray(User.id, memberIds))
|
||||
: [];
|
||||
} catch {
|
||||
} catch (error) {
|
||||
publicReadFailure("guild.member-users")(error);
|
||||
membersFailed = true;
|
||||
users = [];
|
||||
}
|
||||
|
||||
@@ -159,7 +173,7 @@ export default async function GuildPage({
|
||||
<div className="stat-grid" style={{ marginTop: "1.1rem" }}>
|
||||
<StatBlock
|
||||
icon="👥"
|
||||
value={members.length}
|
||||
value={membersFailed ? "—" : members.length}
|
||||
label={
|
||||
members.length === 1 ? t("memberSingular") : t("memberPlural")
|
||||
}
|
||||
@@ -185,11 +199,18 @@ export default async function GuildPage({
|
||||
<ContentCard
|
||||
icon="👥"
|
||||
title={t("membersTitle")}
|
||||
subtitle={`${members.length} ${members.length === 1 ? t("memberLowerSingular") : t("memberLowerPlural")}`}
|
||||
padded={members.length === 0}
|
||||
subtitle={
|
||||
membersFailed
|
||||
? undefined
|
||||
: `${members.length} ${members.length === 1 ? t("memberLowerSingular") : t("memberLowerPlural")}`
|
||||
}
|
||||
padded={members.length === 0 || membersFailed}
|
||||
>
|
||||
{membersFailed && <PublicLoadError href={`/guilds/${guild.id}`} />}
|
||||
{members.length === 0 ? (
|
||||
<EmptyState icon="👥">{t("noMembers")}</EmptyState>
|
||||
membersFailed ? null : (
|
||||
<EmptyState icon="👥">{t("noMembers")}</EmptyState>
|
||||
)
|
||||
) : (
|
||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||
{members.map((m) => (
|
||||
|
||||
@@ -2,9 +2,11 @@ import { desc } from "drizzle-orm";
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "@/components/link";
|
||||
import { PublicLoadError } from "@/components/public/load-error";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { db, Guilds } from "@/lib/db";
|
||||
import { excerpt } from "@/lib/format";
|
||||
import { publicReadFailure } from "@/lib/services/public-read";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("pages.guilds");
|
||||
@@ -26,7 +28,7 @@ type GuildCard = {
|
||||
badge: string;
|
||||
};
|
||||
|
||||
async function getGuilds(): Promise<GuildCard[]> {
|
||||
async function getGuilds(): Promise<GuildCard[] | null> {
|
||||
try {
|
||||
return await db
|
||||
.select({
|
||||
@@ -38,8 +40,8 @@ async function getGuilds(): Promise<GuildCard[]> {
|
||||
.from(Guilds)
|
||||
.orderBy(desc(Guilds.id))
|
||||
.limit(40);
|
||||
} catch {
|
||||
return [];
|
||||
} catch (error) {
|
||||
return publicReadFailure("guilds")(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +53,10 @@ export default async function GuildsPage() {
|
||||
<main className="page-grid">
|
||||
<ContentCard icon="🚪" title={t("title")} subtitle={t("subtitle")} />
|
||||
|
||||
<ContentCard padded={guilds.length === 0}>
|
||||
{guilds.length === 0 ? (
|
||||
<ContentCard padded={!guilds?.length}>
|
||||
{guilds === null ? (
|
||||
<PublicLoadError href="/guilds" />
|
||||
) : guilds.length === 0 ? (
|
||||
<EmptyState icon="🚪">{t("empty")}</EmptyState>
|
||||
) : (
|
||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||
|
||||
Reference in new issue
Block a user