From 0fef258be594598eea1c3282b43643032ba8c459 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Wed, 19 Nov 2025 14:14:12 +0200 Subject: [PATCH] Enhance error handling in route: /c --- apps/cyberstorm-remix/app/c/Community.css | 49 ++- apps/cyberstorm-remix/app/c/community.tsx | 430 +++++++++++++--------- 2 files changed, 280 insertions(+), 199 deletions(-) diff --git a/apps/cyberstorm-remix/app/c/Community.css b/apps/cyberstorm-remix/app/c/Community.css index 224d5287c..b9ef2891b 100644 --- a/apps/cyberstorm-remix/app/c/Community.css +++ b/apps/cyberstorm-remix/app/c/Community.css @@ -5,10 +5,10 @@ z-index: 1; display: flex; flex-direction: column; - gap: 1rem; + gap: var(--space-16); align-items: flex-start; align-self: stretch; - padding: 1rem 3rem 2rem; + padding: var(--space-16) var(--space-48) var(--space-32); } .community__header { @@ -27,14 +27,14 @@ align-self: stretch; justify-content: center; max-height: 12.5rem; - border-radius: 0.5rem; + border-radius: var(--radius-md); overflow-y: hidden; transition: height 2s; > img { width: 100%; height: 100%; - border-radius: 0.5rem; + border-radius: var(--radius-md); object-fit: cover; background: var(--color-ui-surface-1); opacity: 0.8; @@ -58,13 +58,13 @@ z-index: 1; display: flex; flex-wrap: wrap; - gap: 1.5rem; + gap: var(--space-24); place-items: flex-end stretch; align-self: stretch; justify-content: space-between; height: max-content; - margin-top: -1rem; - padding-left: 1rem; + margin-top: calc(var(--space-16) * -1); + padding-left: var(--space-16); transition: height ease 1s, opacity 0.2s, @@ -74,7 +74,7 @@ .community__content-header { display: flex; flex-grow: 1; - gap: 1.5rem; + gap: var(--space-24); align-items: flex-end; align-self: stretch; min-width: 75%; @@ -93,14 +93,14 @@ .community__game-icon { display: flex; - gap: 0.5rem; + gap: var(--space-8); align-items: center; width: 7rem; height: 7rem; - padding: var(--Space-12, 0.75rem); - border: 1px solid var(--Color-border-bright, rgb(70 70 149 / 0.66)); - border-radius: var(--Radius-xl, 1rem); - background: var(--Color-Surface-1, #070721); + padding: var(--space-12); + border: var(--border-width--px) solid var(--color-ui-border-2); + border-radius: var(--radius-xl); + background: var(--color-surface-1); aspect-ratio: 1/1; } @@ -121,7 +121,7 @@ display: flex; flex-direction: column; flex-grow: 1; - gap: 0.75rem; + gap: var(--space-12); align-items: flex-start; justify-content: center; } @@ -130,7 +130,7 @@ display: flex; flex: 1 0 0; flex-direction: column; - gap: 0.25rem; + gap: var(--space-4); align-items: flex-start; align-self: stretch; min-width: 60%; @@ -145,10 +145,10 @@ .community__header-meta { display: flex; flex: 0 1 60%; - gap: 1.5rem; + gap: var(--space-24); align-items: center; min-width: 60%; - height: 16px; + height: var(--space-16); > .skeleton { height: 1rem; @@ -197,6 +197,19 @@ } } + .community__error { + display: flex; + flex-direction: column; + gap: var(--space-16); + align-items: flex-start; + padding: var(--space-48) 0; + } + + .community__error-description { + max-width: 40rem; + color: var(--color-text-tertiary); + } + @media (width >= 41rem) { .community__background { height: 12.5rem; @@ -211,7 +224,7 @@ } .community__header { - gap: 1rem; + gap: var(--space-16); } .community__content-header-wrapper { diff --git a/apps/cyberstorm-remix/app/c/community.tsx b/apps/cyberstorm-remix/app/c/community.tsx index 16733650e..6b42c3da8 100644 --- a/apps/cyberstorm-remix/app/c/community.tsx +++ b/apps/cyberstorm-remix/app/c/community.tsx @@ -2,10 +2,16 @@ import { faDiscord } from "@fortawesome/free-brands-svg-icons"; import { faBook, faDownload } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRight } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables"; import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; + NimbusAwaitErrorElement, + NimbusDefaultRouteErrorBoundary, + NimbusErrorBoundary, +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError"; +import { createNotFoundMapping } from "cyberstorm/utils/errors/loaderMappings"; +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; import { Suspense } from "react"; import type { LoaderFunctionArgs, @@ -32,40 +38,65 @@ import { DapperTs } from "@thunderstore/dapper-ts"; import { type OutletContextShape } from "../root"; import "./Community.css"; +const communityNotFoundMappings = [ + createNotFoundMapping( + "Community not found.", + "We could not find the requested community." + ), +]; + +/** + * Remix server loader that fetches the community by id while translating API errors + * into user-facing payload responses. + */ export async function loader({ params }: LoaderFunctionArgs) { if (params.communityId) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { + const { dapper } = getLoaderTools(); + try { + const community = await dapper.getCommunity(params.communityId); return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, + community, }; - }); - const community = await dapper.getCommunity(params.communityId); - return { - community: community, - }; + } catch (error) { + handleLoaderError(error, { mappings: communityNotFoundMappings }); + } } - throw new Response("Community not found", { status: 404 }); + throwUserFacingPayloadResponse({ + headline: "Community not found.", + description: "We could not find the requested community.", + category: "not_found", + status: 404, + }); } +/** + * Remix client loader that defers community fetching to the browser and maps + * API failures to the same user-facing responses as the server loader. + */ export async function clientLoader({ params }: LoaderFunctionArgs) { if (params.communityId) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); - const community = dapper.getCommunity(params.communityId); + const { dapper } = getLoaderTools(); + return { - community: community, + community: dapper + .getCommunity(params.communityId) + .catch((error) => + handleLoaderError(error, { mappings: communityNotFoundMappings }) + ), }; } - throw new Response("Community not found", { status: 404 }); + throwUserFacingPayloadResponse({ + headline: "Community not found.", + description: "We could not find the requested community.", + category: "not_found", + status: 404, + }); } +/** + * Determines whether the route should revalidate when navigating within the + * community section. + */ export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { if ( arg.currentUrl.pathname.split("/")[1] === arg.nextUrl.pathname.split("/")[1] @@ -75,6 +106,10 @@ export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { return arg.defaultShouldRevalidate; } +/** + * Default community route component that renders the header for the selected + * community and provides Suspense-driven loading states. + */ export default function Community() { const { community } = useLoaderData(); const location = useLocation(); @@ -82,179 +117,212 @@ export default function Community() { const isSubPath = splitPath.length > 4; const isPackageListingSubPath = splitPath.length > 5 && splitPath[1] === "c" && splitPath[3] === "p"; - const outletContext = useOutletContext() as OutletContextShape; + const { VITE_BETA_SITE_URL } = getPublicEnvVariables(["VITE_BETA_SITE_URL"]); return ( <> {isSubPath ? null : ( - - - {(resolvedValue) => ( - <> - - - - - - - - - - - - )} + + }> + {(result) => { + return ( + <> + + + + + + + + + + + + ); + }} )} - <> -
-
- }> - - {(resolvedValue) => - resolvedValue.hero_image_url ? ( - {resolvedValue.name} - ) : null - } - - + }> + }> + {(result) => ( + reset()} + > + + + )} + + + + + + ); +} + +/** + * Renders the community header once the community payload has resolved. + */ +function CommunityHeaderContent(props: { + community: Awaited>; + isPackageListingSubPath: boolean; + isSubPath: boolean; +}) { + const { community, isPackageListingSubPath, isSubPath } = props; + + return ( +
+
+ {community.hero_image_url ? ( + {community.name} + ) : null} +
+ +
+
+
+
+ {community.community_icon_url ? ( + {community.name} + ) : null} +
+
+
+
+ + {community.name} + +
+
+ {community.wiki_url ? ( + + + + + Modding Wiki + + + + + ) : null} + {community.discord_url ? ( + + + + + Modding Discord + + + + + ) : null} +
+
+ + + + + Upload package + +
+
+ ); +} -
-
-
-
- }> - - {(resolvedValue) => - resolvedValue.community_icon_url ? ( - {resolvedValue.name} - ) : null - } - - -
+/** + * Skeleton fallback displayed while community data is loading. + */ +function CommunitySkeleton() { + return ( +
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+
-
-
- }> - - {(resolvedValue) => ( - - {resolvedValue.name} - - )} - - -
-
- }> - - {(resolvedValue) => - resolvedValue.wiki_url ? ( - - - - - Modding Wiki - - - - - ) : null - } - - - }> - - {(resolvedValue) => - resolvedValue.discord_url ? ( - - - - - Modding Discord - - - - - ) : null - } - - -
+
+
- - - - - Upload package -
- - - +
+ +
+
+
); } + +export function ErrorBoundary() { + return ; +}