From 0fb2bac102ebab5ec1d0ab077f7a78e84c23f1d3 Mon Sep 17 00:00:00 2001 From: wotan-allfather Date: Tue, 10 Mar 2026 22:34:28 +0000 Subject: [PATCH] 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 --- apps/web/src/pages/404.tsx | 55 +++++++++++++++++++ apps/web/src/views/board/index.tsx | 10 ++++ apps/web/src/views/card/index.tsx | 11 +++- apps/web/src/views/public/board/CardModal.tsx | 13 ++++- apps/web/src/views/public/boards/index.tsx | 12 +++- 5 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/pages/404.tsx diff --git a/apps/web/src/pages/404.tsx b/apps/web/src/pages/404.tsx new file mode 100644 index 00000000..3f1069ae --- /dev/null +++ b/apps/web/src/pages/404.tsx @@ -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 ( + <> + +
+
+
+ +

+ kan.bn +

+ +

+ 404 +

+

+ Page not found +

+
+
+

+ + The page you're looking for doesn't exist or has been moved. + +

+
+ + Go to homepage + + + Go to boards + +
+
+
+
+ +
+
+ + ); +} diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index 6074a56e..d0d400a6 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -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 }); }; diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index 49a86cff..7eacfc43 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -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; diff --git a/apps/web/src/views/public/board/CardModal.tsx b/apps/web/src/views/public/board/CardModal.tsx index ed555e88..27a95b34 100644 --- a/apps/web/src/views/public/board/CardModal.tsx +++ b/apps/web/src/views/public/board/CardModal.tsx @@ -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 = () => { diff --git a/apps/web/src/views/public/boards/index.tsx b/apps/web/src/views/public/boards/index.tsx index 0830bf60..5fcf0eab 100644 --- a/apps/web/src/views/public/boards/index.tsx +++ b/apps/web/src/views/public/boards/index.tsx @@ -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,