fix: add 404 not found page (#349)

* fix: add 404 not found page

Closes #320

- Add custom 404 page matching the app's design system
- Include PatternedBackground and proper dark mode support
- Provide navigation links to homepage and boards
- Add i18n support with @lingui/react

* fix: add 404 redirects for non-existent boards and cards

Add useEffect hooks to redirect to /404 page when:
- Board does not exist in board view
- Card does not exist in card view

This addresses the cases mentioned in maintainer feedback where
the static 404 page wasn't being used for dynamic routes.

Addresses review feedback on PR #349

* feat: redirect to 404 for non-existent public workspaces and cards

Per maintainer feedback:
- Add redirect logic to public boards view when workspace doesn't exist
- Add redirect logic to public card modal when card doesn't exist
- Uses error?.data?.code === 'NOT_FOUND' check as suggested
- Closes modal before redirecting for card modal
This commit is contained in:
wotan-allfather
2026-03-10 22:34:28 +00:00
committed by GitHub
parent a7b71ae764
commit 0fb2bac102
5 changed files with 98 additions and 3 deletions

View File

@@ -0,0 +1,55 @@
import Link from "next/link";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { PageHead } from "~/components/PageHead";
import PatternedBackground from "~/components/PatternedBackground";
export default function NotFoundPage() {
return (
<>
<PageHead title={t`404 - Page Not Found | kan.bn`} />
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
<div className="z-10 flex w-full flex-col items-center">
<Link href="/">
<h1 className="mb-6 text-lg font-bold tracking-tight text-light-1000 dark:text-dark-1000">
kan.bn
</h1>
</Link>
<p className="mb-4 text-8xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
404
</p>
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
<Trans>Page not found</Trans>
</p>
<div className="w-full rounded-lg border border-light-500 bg-light-300 px-4 py-10 dark:border-dark-400 dark:bg-dark-200 sm:max-w-md lg:px-10">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<p className="mb-6 text-center text-light-900 dark:text-dark-900">
<Trans>
The page you're looking for doesn't exist or has been moved.
</Trans>
</p>
<div className="flex flex-col gap-3">
<Link
href="/"
className="flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-100 shadow-sm hover:bg-light-900 dark:bg-dark-1000 dark:text-dark-100 dark:hover:bg-dark-900"
>
<Trans>Go to homepage</Trans>
</Link>
<Link
href="/boards"
className="flex w-full justify-center rounded-md border border-light-500 bg-light-100 px-3 py-2 text-sm font-semibold text-light-1000 shadow-sm hover:bg-light-200 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 dark:hover:bg-dark-300"
>
<Trans>Go to boards</Trans>
</Link>
</div>
</div>
</div>
</div>
<PatternedBackground />
</div>
</main>
</>
);
}

View File

@@ -125,11 +125,21 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
data: boardData,
isSuccess,
isLoading: isQueryLoading,
error,
} = api.board.byId.useQuery(queryParams, {
enabled: !!boardId,
placeholderData: keepPreviousData,
});
// Redirect to 404 if board doesn't exist
useEffect(() => {
if (router.isReady && boardId && !isQueryLoading) {
if (error?.data?.code === "NOT_FOUND" || (!boardData && !isQueryLoading)) {
router.replace("/404");
}
}
}, [router, boardId, isQueryLoading, error, boardData]);
const refetchBoard = async () => {
if (boardId) await utils.board.byId.refetch({ boardPublicId: boardId });
};

View File

@@ -185,11 +185,20 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card, isLoading } = api.card.byId.useQuery(
const { data: card, isLoading, error } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
// Redirect to 404 if card doesn't exist
useEffect(() => {
if (router.isReady && cardId && !isLoading) {
if (error?.data?.code === "NOT_FOUND" || (!card && !isLoading)) {
router.replace("/404");
}
}
}, [router, cardId, isLoading, error, card]);
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;

View File

@@ -47,7 +47,7 @@ export function CardModal({
}
};
const { data, isLoading } = api.card.byId.useQuery(
const { data, isLoading, error } = api.card.byId.useQuery(
{
cardPublicId: cardPublicId ?? "",
},
@@ -56,6 +56,17 @@ export function CardModal({
},
);
// Redirect to 404 if card doesn't exist
useEffect(() => {
if (isOpen && cardPublicId && !isLoading) {
if (error?.data?.code === "NOT_FOUND" || (!data && !isLoading && error)) {
// Close modal first, then redirect
closeModal();
router.replace("/404");
}
}
}, [isOpen, cardPublicId, isLoading, error, data, closeModal, router]);
const labels = data?.labels ?? [];
const handleScroll = () => {

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { t } from "@lingui/core/macro";
import { PageHead } from "~/components/PageHead";
import PatternedBackground from "~/components/PatternedBackground";
@@ -12,13 +13,22 @@ export default function PublicBoardsView() {
? router.query.workspaceSlug[0]
: router.query.workspaceSlug;
const { data, isLoading } = api.workspace.bySlug.useQuery(
const { data, isLoading, error } = api.workspace.bySlug.useQuery(
{
workspaceSlug: workspaceSlug ?? "",
},
{ enabled: !!workspaceSlug },
);
// Redirect to 404 if workspace doesn't exist
useEffect(() => {
if (router.isReady && workspaceSlug && !isLoading) {
if (error?.data?.code === "NOT_FOUND" || (!data && !isLoading)) {
router.replace("/404");
}
}
}, [router, workspaceSlug, isLoading, error, data]);
const BoardsList = ({
isLoading,
boards,