From c678dc38be4c651a649ca928edda9d9fcc707ec0 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Wed, 19 Nov 2025 14:16:45 +0200 Subject: [PATCH] Enhance error handling in route: /communities --- .../app/communities/Communities.css | 13 +++ .../app/communities/communities.tsx | 106 +++++++++++------- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/apps/cyberstorm-remix/app/communities/Communities.css b/apps/cyberstorm-remix/app/communities/Communities.css index ad7868656..e24b2292e 100644 --- a/apps/cyberstorm-remix/app/communities/Communities.css +++ b/apps/cyberstorm-remix/app/communities/Communities.css @@ -42,6 +42,19 @@ width: 100%; } + .communities__error { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + padding: 3rem 0; + } + + .communities__error-description { + max-width: 40rem; + color: var(--Color-text-muted, rgb(180 189 255 / 0.8)); + } + .communities__community-skeleton { .communities__community-skeleton-image { display: flex; diff --git a/apps/cyberstorm-remix/app/communities/communities.tsx b/apps/cyberstorm-remix/app/communities/communities.tsx index a4f10ed99..696fe60e6 100644 --- a/apps/cyberstorm-remix/app/communities/communities.tsx +++ b/apps/cyberstorm-remix/app/communities/communities.tsx @@ -6,9 +6,11 @@ import { import { faFire, faGhost } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; + NimbusAwaitErrorElement, + NimbusDefaultRouteErrorBoundary, +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError"; +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; import { Suspense, memo, useEffect, useRef, useState } from "react"; import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { @@ -28,10 +30,12 @@ import { SkeletonBox, } from "@thunderstore/cyberstorm"; import type { Communities } from "@thunderstore/dapper"; -import { DapperTs } from "@thunderstore/dapper-ts"; import "./Communities.css"; +/** + * Provides the HTML metadata for the communities listing route. + */ export const meta: MetaFunction = () => { return [ { title: "Communities | Thunderstore" }, @@ -67,48 +71,63 @@ const selectOptions = [ }, ]; -export async function loader({ request }: LoaderFunctionArgs) { +interface CommunitiesQuery { + order: SortOptions; + search: string | undefined; +} + +/** + * Extracts the current query parameters governing the communities list. + */ +function resolveCommunitiesQuery(request: Request): CommunitiesQuery { const searchParams = new URL(request.url).searchParams; - const order = searchParams.get("order") ?? SortOptions.Popular; - const search = searchParams.get("search"); - const page = undefined; - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); + const orderParam = searchParams.get("order"); + const orderValues = Object.values(SortOptions); + const order = + orderParam && orderValues.includes(orderParam as SortOptions) + ? (orderParam as SortOptions) + : SortOptions.Popular; + const search = searchParams.get("search") ?? undefined; + return { - communities: await dapper.getCommunities( - page, - order === null ? undefined : order, - search === null ? undefined : search - ), + order, + search, }; } -export async function clientLoader({ request }: LoaderFunctionArgs) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { +/** + * Fetches communities data on the server and surfaces mapped loader errors. + */ +export async function loader({ request }: LoaderFunctionArgs) { + const query = resolveCommunitiesQuery(request); + const page = undefined; + const { dapper } = getLoaderTools(); + try { return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, + communities: await dapper.getCommunities(page, query.order, query.search), }; - }); - const searchParams = new URL(request.url).searchParams; - const order = searchParams.get("order"); - const search = searchParams.get("search"); + } catch (error) { + handleLoaderError(error); + } +} + +/** + * Fetches communities data on the client, returning a Suspense-ready promise wrapper. + */ +export function clientLoader({ request }: LoaderFunctionArgs) { + const { dapper } = getLoaderTools(); + const query = resolveCommunitiesQuery(request); const page = undefined; return { - communities: dapper.getCommunities( - page, - order ?? SortOptions.Popular, - search ?? "" - ), + communities: dapper + .getCommunities(page, query.order, query.search) + .catch((error) => handleLoaderError(error)), }; } +/** + * Renders the communities listing experience with search, sorting, and Suspense fallback handling. + */ export default function CommunitiesPage() { const { communities } = useLoaderData(); const navigationType = useNavigationType(); @@ -117,6 +136,9 @@ export default function CommunitiesPage() { // TODO: Disabled until we can figure out how a proper way to display skeletons // const navigation = useNavigation(); + /** + * Persists the selected sort order back into the URL search params. + */ const changeOrder = (v: SortOptions) => { if (v === SortOptions.Popular) { searchParams.delete("order"); @@ -192,11 +214,9 @@ export default function CommunitiesPage() { }> Error loading communities} + errorElement={} > - {(resolvedValue) => ( - - )} + {(result) => } @@ -205,6 +225,13 @@ export default function CommunitiesPage() { ); } +export function ErrorBoundary() { + return ; +} + +/** + * Displays the resolved communities list or an empty state when no entries exist. + */ const CommunitiesList = memo(function CommunitiesList(props: { communitiesData: Communities; }) { @@ -238,6 +265,9 @@ const CommunitiesList = memo(function CommunitiesList(props: { } }); +/** + * Shows a skeleton grid while the communities listing resolves. + */ const CommunitiesListSkeleton = memo(function CommunitiesListSkeleton() { return (