@@ -32,6 +32,9 @@ import type { Communities } from "@thunderstore/dapper/types";
3232
3333import "./Communities.css" ;
3434
35+ /**
36+ * Provides the HTML metadata for the communities listing route.
37+ */
3538export const meta : MetaFunction = ( ) => {
3639 return [
3740 { title : "Communities | Thunderstore" } ,
@@ -67,48 +70,63 @@ const selectOptions = [
6770 } ,
6871] ;
6972
70- export async function loader ( { request } : LoaderFunctionArgs ) {
73+ interface CommunitiesQuery {
74+ order : SortOptions ;
75+ search : string | undefined ;
76+ }
77+
78+ /**
79+ * Extracts the current query parameters governing the communities list.
80+ */
81+ function resolveCommunitiesQuery ( request : Request ) : CommunitiesQuery {
7182 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- } ) ;
83+ const orderParam = searchParams . get ( "order" ) ;
84+ const orderValues = Object . values ( SortOptions ) ;
85+ const order =
86+ orderParam && orderValues . includes ( orderParam as SortOptions )
87+ ? ( orderParam as SortOptions )
88+ : SortOptions . Popular ;
89+ const search = searchParams . get ( "search" ) ?? undefined ;
90+
8291 return {
83- communities : await dapper . getCommunities (
84- page ,
85- order === null ? undefined : order ,
86- search === null ? undefined : search
87- ) ,
92+ order,
93+ search,
8894 } ;
8995}
9096
91- export async function clientLoader ( { request } : LoaderFunctionArgs ) {
92- const tools = getSessionTools ( ) ;
93- const dapper = new DapperTs ( ( ) => {
97+ /**
98+ * Fetches communities data on the server and surfaces mapped loader errors.
99+ */
100+ export async function loader ( { request } : LoaderFunctionArgs ) {
101+ const query = resolveCommunitiesQuery ( request ) ;
102+ const page = undefined ;
103+ const { dapper } = getLoaderTools ( ) ;
104+ try {
94105 return {
95- apiHost : tools ?. getConfig ( ) . apiHost ,
96- sessionId : tools ?. getConfig ( ) . sessionId ,
106+ communities : await dapper . getCommunities ( page , query . order , query . search ) ,
97107 } ;
98- } ) ;
99- const searchParams = new URL ( request . url ) . searchParams ;
100- const order = searchParams . get ( "order" ) ;
101- const search = searchParams . get ( "search" ) ;
108+ } catch ( error ) {
109+ handleLoaderError ( error ) ;
110+ }
111+ }
112+
113+ /**
114+ * Fetches communities data on the client, returning a Suspense-ready promise wrapper.
115+ */
116+ export function clientLoader ( { request } : LoaderFunctionArgs ) {
117+ const { dapper } = getLoaderTools ( ) ;
118+ const query = resolveCommunitiesQuery ( request ) ;
102119 const page = undefined ;
103120 return {
104- communities : dapper . getCommunities (
105- page ,
106- order ?? SortOptions . Popular ,
107- search ?? ""
108- ) ,
121+ communities : dapper
122+ . getCommunities ( page , query . order , query . search )
123+ . catch ( ( error ) => handleLoaderError ( error ) ) ,
109124 } ;
110125}
111126
127+ /**
128+ * Renders the communities listing experience with search, sorting, and Suspense fallback handling.
129+ */
112130export default function CommunitiesPage ( ) {
113131 const { communities } = useLoaderData < typeof loader | typeof clientLoader > ( ) ;
114132 const navigationType = useNavigationType ( ) ;
@@ -117,6 +135,9 @@ export default function CommunitiesPage() {
117135 // TODO: Disabled until we can figure out how a proper way to display skeletons
118136 // const navigation = useNavigation();
119137
138+ /**
139+ * Persists the selected sort order back into the URL search params.
140+ */
120141 const changeOrder = ( v : SortOptions ) => {
121142 if ( v === SortOptions . Popular ) {
122143 searchParams . delete ( "order" ) ;
@@ -192,11 +213,9 @@ export default function CommunitiesPage() {
192213 < Suspense fallback = { < CommunitiesListSkeleton /> } >
193214 < Await
194215 resolve = { communities }
195- errorElement = { < div > Error loading communities </ div > }
216+ errorElement = { < NimbusAwaitErrorElement / >}
196217 >
197- { ( resolvedValue ) => (
198- < CommunitiesList communitiesData = { resolvedValue } />
199- ) }
218+ { ( result ) => < CommunitiesList communitiesData = { result } /> }
200219 </ Await >
201220 </ Suspense >
202221 </ div >
@@ -205,6 +224,13 @@ export default function CommunitiesPage() {
205224 ) ;
206225}
207226
227+ export function ErrorBoundary ( ) {
228+ return < NimbusDefaultRouteErrorBoundary /> ;
229+ }
230+
231+ /**
232+ * Displays the resolved communities list or an empty state when no entries exist.
233+ */
208234const CommunitiesList = memo ( function CommunitiesList ( props : {
209235 communitiesData : Communities ;
210236} ) {
@@ -238,6 +264,9 @@ const CommunitiesList = memo(function CommunitiesList(props: {
238264 }
239265} ) ;
240266
267+ /**
268+ * Shows a skeleton grid while the communities listing resolves.
269+ */
241270const CommunitiesListSkeleton = memo ( function CommunitiesListSkeleton ( ) {
242271 return (
243272 < div className = "communities__communities-list" >
0 commit comments