@@ -23,13 +23,17 @@ import {
2323 useSearchParams ,
2424} from "react-router" ;
2525import type { Communities } from "@thunderstore/dapper/types" ;
26- import { DapperTs } from "@thunderstore/dapper-ts" ;
2726import { PageHeader } from "~/commonComponents/PageHeader/PageHeader" ;
2827import {
29- getPublicEnvVariables ,
30- getSessionTools ,
31- } from "cyberstorm/security/publicEnvVariables" ;
28+ NimbusAwaitErrorElement ,
29+ NimbusDefaultRouteErrorBoundary ,
30+ } from "cyberstorm/utils/errors/NimbusErrorBoundary" ;
31+ import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError" ;
32+ import { getLoaderTools } from "cyberstorm/utils/getLoaderTools" ;
3233
34+ /**
35+ * Provides the HTML metadata for the communities listing route.
36+ */
3337export const meta : MetaFunction = ( ) => {
3438 return [
3539 { title : "Communities | Thunderstore" } ,
@@ -65,48 +69,63 @@ const selectOptions = [
6569 } ,
6670] ;
6771
68- export async function loader ( { request } : LoaderFunctionArgs ) {
72+ interface CommunitiesQuery {
73+ order : SortOptions ;
74+ search : string | undefined ;
75+ }
76+
77+ /**
78+ * Extracts the current query parameters governing the communities list.
79+ */
80+ function resolveCommunitiesQuery ( request : Request ) : CommunitiesQuery {
6981 const searchParams = new URL ( request . url ) . searchParams ;
70- const order = searchParams . get ( "order" ) ?? SortOptions . Popular ;
71- const search = searchParams . get ( "search" ) ;
72- const page = undefined ;
73- const publicEnvVariables = getPublicEnvVariables ( [ "VITE_API_URL" ] ) ;
74- const dapper = new DapperTs ( ( ) => {
75- return {
76- apiHost : publicEnvVariables . VITE_API_URL ,
77- sessionId : undefined ,
78- } ;
79- } ) ;
82+ const orderParam = searchParams . get ( "order" ) ;
83+ const orderValues = Object . values ( SortOptions ) ;
84+ const order =
85+ orderParam && orderValues . includes ( orderParam as SortOptions )
86+ ? ( orderParam as SortOptions )
87+ : SortOptions . Popular ;
88+ const search = searchParams . get ( "search" ) ?? undefined ;
89+
8090 return {
81- communities : await dapper . getCommunities (
82- page ,
83- order === null ? undefined : order ,
84- search === null ? undefined : search
85- ) ,
91+ order,
92+ search,
8693 } ;
8794}
8895
89- export async function clientLoader ( { request } : LoaderFunctionArgs ) {
90- const tools = getSessionTools ( ) ;
91- const dapper = new DapperTs ( ( ) => {
96+ /**
97+ * Fetches communities data on the server and surfaces mapped loader errors.
98+ */
99+ export async function loader ( { request } : LoaderFunctionArgs ) {
100+ const query = resolveCommunitiesQuery ( request ) ;
101+ const page = undefined ;
102+ const { dapper } = getLoaderTools ( ) ;
103+ try {
92104 return {
93- apiHost : tools ?. getConfig ( ) . apiHost ,
94- sessionId : tools ?. getConfig ( ) . sessionId ,
105+ communities : await dapper . getCommunities ( page , query . order , query . search ) ,
95106 } ;
96- } ) ;
97- const searchParams = new URL ( request . url ) . searchParams ;
98- const order = searchParams . get ( "order" ) ;
99- const search = searchParams . get ( "search" ) ;
107+ } catch ( error ) {
108+ handleLoaderError ( error ) ;
109+ }
110+ }
111+
112+ /**
113+ * Fetches communities data on the client, returning a Suspense-ready promise wrapper.
114+ */
115+ export function clientLoader ( { request } : LoaderFunctionArgs ) {
116+ const { dapper } = getLoaderTools ( ) ;
117+ const query = resolveCommunitiesQuery ( request ) ;
100118 const page = undefined ;
101119 return {
102- communities : dapper . getCommunities (
103- page ,
104- order ?? SortOptions . Popular ,
105- search ?? ""
106- ) ,
120+ communities : dapper
121+ . getCommunities ( page , query . order , query . search )
122+ . catch ( ( error ) => handleLoaderError ( error ) ) ,
107123 } ;
108124}
109125
126+ /**
127+ * Renders the communities listing experience with search, sorting, and Suspense fallback handling.
128+ */
110129export default function CommunitiesPage ( ) {
111130 const { communities } = useLoaderData < typeof loader | typeof clientLoader > ( ) ;
112131 const navigationType = useNavigationType ( ) ;
@@ -115,6 +134,9 @@ export default function CommunitiesPage() {
115134 // TODO: Disabled until we can figure out how a proper way to display skeletons
116135 // const navigation = useNavigation();
117136
137+ /**
138+ * Persists the selected sort order back into the URL search params.
139+ */
118140 const changeOrder = ( v : SortOptions ) => {
119141 if ( v === SortOptions . Popular ) {
120142 searchParams . delete ( "order" ) ;
@@ -190,11 +212,9 @@ export default function CommunitiesPage() {
190212 < Suspense fallback = { < CommunitiesListSkeleton /> } >
191213 < Await
192214 resolve = { communities }
193- errorElement = { < div > Error loading communities </ div > }
215+ errorElement = { < NimbusAwaitErrorElement / >}
194216 >
195- { ( resolvedValue ) => (
196- < CommunitiesList communitiesData = { resolvedValue } />
197- ) }
217+ { ( result ) => < CommunitiesList communitiesData = { result } /> }
198218 </ Await >
199219 </ Suspense >
200220 </ div >
@@ -203,6 +223,13 @@ export default function CommunitiesPage() {
203223 ) ;
204224}
205225
226+ export function ErrorBoundary ( ) {
227+ return < NimbusDefaultRouteErrorBoundary /> ;
228+ }
229+
230+ /**
231+ * Displays the resolved communities list or an empty state when no entries exist.
232+ */
206233const CommunitiesList = memo ( function CommunitiesList ( props : {
207234 communitiesData : Communities ;
208235} ) {
@@ -236,6 +263,9 @@ const CommunitiesList = memo(function CommunitiesList(props: {
236263 }
237264} ) ;
238265
266+ /**
267+ * Shows a skeleton grid while the communities listing resolves.
268+ */
239269const CommunitiesListSkeleton = memo ( function CommunitiesListSkeleton ( ) {
240270 return (
241271 < div className = "communities__communities-list" >
0 commit comments