@@ -6,9 +6,11 @@ import {
66import { faFire , faGhost } from "@fortawesome/free-solid-svg-icons" ;
77import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
88import {
9- getPublicEnvVariables ,
10- getSessionTools ,
11- } from "cyberstorm/security/publicEnvVariables" ;
9+ NimbusAwaitErrorElement ,
10+ NimbusDefaultRouteErrorBoundary ,
11+ } from "cyberstorm/utils/errors/NimbusErrorBoundary" ;
12+ import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError" ;
13+ import { getLoaderTools } from "cyberstorm/utils/getLoaderTools" ;
1214import { Suspense , memo , useEffect , useRef , useState } from "react" ;
1315import type { LoaderFunctionArgs , MetaFunction } from "react-router" ;
1416import {
@@ -28,10 +30,12 @@ import {
2830 SkeletonBox ,
2931} from "@thunderstore/cyberstorm" ;
3032import type { Communities } from "@thunderstore/dapper" ;
31- import { DapperTs } from "@thunderstore/dapper-ts" ;
3233
3334import "./Communities.css" ;
3435
36+ /**
37+ * Provides the HTML metadata for the communities listing route.
38+ */
3539export const meta : MetaFunction = ( ) => {
3640 return [
3741 { title : "Communities | Thunderstore" } ,
@@ -67,48 +71,63 @@ const selectOptions = [
6771 } ,
6872] ;
6973
70- export async function loader ( { request } : LoaderFunctionArgs ) {
74+ interface CommunitiesQuery {
75+ order : SortOptions ;
76+ search : string | undefined ;
77+ }
78+
79+ /**
80+ * Extracts the current query parameters governing the communities list.
81+ */
82+ function resolveCommunitiesQuery ( request : Request ) : CommunitiesQuery {
7183 const searchParams = new URL ( request . url ) . searchParams ;
72- const order = searchParams . get ( "order" ) ?? SortOptions . Popular ;
73- const search = searchParams . get ( "search" ) ;
74- const page = undefined ;
75- const publicEnvVariables = getPublicEnvVariables ( [ "VITE_API_URL" ] ) ;
76- const dapper = new DapperTs ( ( ) => {
77- return {
78- apiHost : publicEnvVariables . VITE_API_URL ,
79- sessionId : undefined ,
80- } ;
81- } ) ;
84+ const orderParam = searchParams . get ( "order" ) ;
85+ const orderValues = Object . values ( SortOptions ) ;
86+ const order =
87+ orderParam && orderValues . includes ( orderParam as SortOptions )
88+ ? ( orderParam as SortOptions )
89+ : SortOptions . Popular ;
90+ const search = searchParams . get ( "search" ) ?? undefined ;
91+
8292 return {
83- communities : await dapper . getCommunities (
84- page ,
85- order === null ? undefined : order ,
86- search === null ? undefined : search
87- ) ,
93+ order,
94+ search,
8895 } ;
8996}
9097
91- export async function clientLoader ( { request } : LoaderFunctionArgs ) {
92- const tools = getSessionTools ( ) ;
93- const dapper = new DapperTs ( ( ) => {
98+ /**
99+ * Fetches communities data on the server and surfaces mapped loader errors.
100+ */
101+ export async function loader ( { request } : LoaderFunctionArgs ) {
102+ const query = resolveCommunitiesQuery ( request ) ;
103+ const page = undefined ;
104+ const { dapper } = getLoaderTools ( ) ;
105+ try {
94106 return {
95- apiHost : tools ?. getConfig ( ) . apiHost ,
96- sessionId : tools ?. getConfig ( ) . sessionId ,
107+ communities : await dapper . getCommunities ( page , query . order , query . search ) ,
97108 } ;
98- } ) ;
99- const searchParams = new URL ( request . url ) . searchParams ;
100- const order = searchParams . get ( "order" ) ;
101- const search = searchParams . get ( "search" ) ;
109+ } catch ( error ) {
110+ handleLoaderError ( error ) ;
111+ }
112+ }
113+
114+ /**
115+ * Fetches communities data on the client, returning a Suspense-ready promise wrapper.
116+ */
117+ export function clientLoader ( { request } : LoaderFunctionArgs ) {
118+ const { dapper } = getLoaderTools ( ) ;
119+ const query = resolveCommunitiesQuery ( request ) ;
102120 const page = undefined ;
103121 return {
104- communities : dapper . getCommunities (
105- page ,
106- order ?? SortOptions . Popular ,
107- search ?? ""
108- ) ,
122+ communities : dapper
123+ . getCommunities ( page , query . order , query . search )
124+ . catch ( ( error ) => handleLoaderError ( error ) ) ,
109125 } ;
110126}
111127
128+ /**
129+ * Renders the communities listing experience with search, sorting, and Suspense fallback handling.
130+ */
112131export default function CommunitiesPage ( ) {
113132 const { communities } = useLoaderData < typeof loader | typeof clientLoader > ( ) ;
114133 const navigationType = useNavigationType ( ) ;
@@ -117,6 +136,9 @@ export default function CommunitiesPage() {
117136 // TODO: Disabled until we can figure out how a proper way to display skeletons
118137 // const navigation = useNavigation();
119138
139+ /**
140+ * Persists the selected sort order back into the URL search params.
141+ */
120142 const changeOrder = ( v : SortOptions ) => {
121143 if ( v === SortOptions . Popular ) {
122144 searchParams . delete ( "order" ) ;
@@ -192,11 +214,9 @@ export default function CommunitiesPage() {
192214 < Suspense fallback = { < CommunitiesListSkeleton /> } >
193215 < Await
194216 resolve = { communities }
195- errorElement = { < div > Error loading communities </ div > }
217+ errorElement = { < NimbusAwaitErrorElement / >}
196218 >
197- { ( resolvedValue ) => (
198- < CommunitiesList communitiesData = { resolvedValue } />
199- ) }
219+ { ( result ) => < CommunitiesList communitiesData = { result } /> }
200220 </ Await >
201221 </ Suspense >
202222 </ div >
@@ -205,6 +225,13 @@ export default function CommunitiesPage() {
205225 ) ;
206226}
207227
228+ export function ErrorBoundary ( ) {
229+ return < NimbusDefaultRouteErrorBoundary /> ;
230+ }
231+
232+ /**
233+ * Displays the resolved communities list or an empty state when no entries exist.
234+ */
208235const CommunitiesList = memo ( function CommunitiesList ( props : {
209236 communitiesData : Communities ;
210237} ) {
@@ -238,6 +265,9 @@ const CommunitiesList = memo(function CommunitiesList(props: {
238265 }
239266} ) ;
240267
268+ /**
269+ * Shows a skeleton grid while the communities listing resolves.
270+ */
241271const CommunitiesListSkeleton = memo ( function CommunitiesListSkeleton ( ) {
242272 return (
243273 < div className = "communities__communities-list" >
0 commit comments