|
1 | | -import { |
2 | | - getPublicEnvVariables, |
3 | | - getSessionTools, |
4 | | -} from "cyberstorm/security/publicEnvVariables"; |
5 | | -import { useLoaderData, useOutletContext } from "react-router"; |
| 1 | +import { Await, useLoaderData, useOutletContext } from "react-router"; |
| 2 | +import "./Team.css"; |
6 | 3 | import { PackageSearch } from "~/commonComponents/PackageSearch/PackageSearch"; |
7 | | -import { PageHeader } from "~/commonComponents/PageHeader/PageHeader"; |
8 | | - |
9 | | -import { DapperTs } from "@thunderstore/dapper-ts"; |
10 | | - |
11 | 4 | import { PackageOrderOptions } from "../../commonComponents/PackageSearch/components/PackageOrder"; |
12 | 5 | import { type OutletContextShape } from "../../root"; |
| 6 | +import { PageHeader } from "~/commonComponents/PageHeader/PageHeader"; |
13 | 7 | import type { Route } from "./+types/Team"; |
14 | | -import "./Team.css"; |
| 8 | +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; |
| 9 | +import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError"; |
| 10 | +import { createNotFoundMapping } from "cyberstorm/utils/errors/loaderMappings"; |
| 11 | +import { SkeletonBox } from "@thunderstore/cyberstorm"; |
| 12 | +import { Suspense } from "react"; |
| 13 | +import { |
| 14 | + NimbusAwaitErrorElement, |
| 15 | + NimbusDefaultRouteErrorBoundary, |
| 16 | +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; |
| 17 | +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; |
| 18 | +import { parseIntegerSearchParam } from "cyberstorm/utils/searchParamsUtils"; |
| 19 | + |
| 20 | +const teamNotFoundMappings = [ |
| 21 | + createNotFoundMapping( |
| 22 | + "Team not found.", |
| 23 | + "We could not find the requested team.", |
| 24 | + 404 |
| 25 | + ), |
| 26 | +]; |
15 | 27 |
|
16 | 28 | export async function loader({ params, request }: Route.LoaderArgs) { |
17 | 29 | if (params.communityId && params.namespaceId) { |
18 | | - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); |
19 | | - const dapper = new DapperTs(() => { |
20 | | - return { |
21 | | - apiHost: publicEnvVariables.VITE_API_URL, |
22 | | - sessionId: undefined, |
23 | | - }; |
24 | | - }); |
| 30 | + const { dapper } = getLoaderTools(); |
25 | 31 | const searchParams = new URL(request.url).searchParams; |
26 | 32 | const ordering = |
27 | 33 | searchParams.get("ordering") ?? PackageOrderOptions.Updated; |
28 | | - const page = searchParams.get("page"); |
| 34 | + const page = parseIntegerSearchParam(searchParams.get("page")); |
29 | 35 | const search = searchParams.get("search"); |
30 | 36 | const includedCategories = searchParams.get("includedCategories"); |
31 | 37 | const excludedCategories = searchParams.get("excludedCategories"); |
32 | 38 | const section = searchParams.get("section"); |
33 | 39 | const nsfw = searchParams.get("nsfw"); |
34 | 40 | const deprecated = searchParams.get("deprecated"); |
35 | | - const filters = await dapper.getCommunityFilters(params.communityId); |
36 | | - |
37 | | - return { |
38 | | - teamId: params.namespaceId, |
39 | | - filters: filters, |
40 | | - listings: await dapper.getPackageListings( |
| 41 | + try { |
| 42 | + const filters = await dapper.getCommunityFilters(params.communityId); |
| 43 | + const listings = await dapper.getPackageListings( |
41 | 44 | { |
42 | 45 | kind: "namespace", |
43 | 46 | communityId: params.communityId, |
44 | 47 | namespaceId: params.namespaceId, |
45 | 48 | }, |
46 | 49 | ordering ?? "", |
47 | | - page === null ? undefined : Number(page), |
| 50 | + page, |
48 | 51 | search ?? "", |
49 | 52 | includedCategories?.split(",") ?? undefined, |
50 | 53 | excludedCategories?.split(",") ?? undefined, |
51 | 54 | section ? (section === "all" ? "" : section) : "", |
52 | | - nsfw === "true" ? true : false, |
53 | | - deprecated === "true" ? true : false |
54 | | - ), |
55 | | - }; |
| 55 | + nsfw === "true", |
| 56 | + deprecated === "true" |
| 57 | + ); |
| 58 | + const dataPromise = Promise.all([filters, listings]); |
| 59 | + |
| 60 | + return { |
| 61 | + teamId: params.namespaceId, |
| 62 | + filtersAndListings: await dataPromise, |
| 63 | + }; |
| 64 | + } catch (error) { |
| 65 | + handleLoaderError(error, { mappings: teamNotFoundMappings }); |
| 66 | + } |
56 | 67 | } |
57 | | - throw new Response("Community not found", { status: 404 }); |
| 68 | + throwUserFacingPayloadResponse({ |
| 69 | + headline: "Community not found.", |
| 70 | + description: "We could not find the requested community.", |
| 71 | + category: "not_found", |
| 72 | + status: 404, |
| 73 | + }); |
58 | 74 | } |
59 | 75 |
|
60 | 76 | export async function clientLoader({ |
61 | 77 | request, |
62 | 78 | params, |
63 | 79 | }: Route.ClientLoaderArgs) { |
64 | 80 | if (params.communityId && params.namespaceId) { |
65 | | - const tools = getSessionTools(); |
66 | | - const dapper = new DapperTs(() => { |
67 | | - return { |
68 | | - apiHost: tools?.getConfig().apiHost, |
69 | | - sessionId: tools?.getConfig().sessionId, |
70 | | - }; |
71 | | - }); |
| 81 | + const { dapper } = getLoaderTools(); |
72 | 82 | const searchParams = new URL(request.url).searchParams; |
73 | 83 | const ordering = |
74 | 84 | searchParams.get("ordering") ?? PackageOrderOptions.Updated; |
75 | | - const page = searchParams.get("page"); |
| 85 | + const page = parseIntegerSearchParam(searchParams.get("page")); |
76 | 86 | const search = searchParams.get("search"); |
77 | 87 | const includedCategories = searchParams.get("includedCategories"); |
78 | 88 | const excludedCategories = searchParams.get("excludedCategories"); |
79 | 89 | const section = searchParams.get("section"); |
80 | 90 | const nsfw = searchParams.get("nsfw"); |
81 | 91 | const deprecated = searchParams.get("deprecated"); |
82 | | - const filters = dapper.getCommunityFilters(params.communityId); |
83 | | - return { |
84 | | - teamId: params.namespaceId, |
85 | | - filters: filters, |
86 | | - listings: dapper.getPackageListings( |
| 92 | + const filters = dapper |
| 93 | + .getCommunityFilters(params.communityId) |
| 94 | + .catch((error) => |
| 95 | + handleLoaderError(error, { mappings: teamNotFoundMappings }) |
| 96 | + ); |
| 97 | + const listings = dapper |
| 98 | + .getPackageListings( |
87 | 99 | { |
88 | 100 | kind: "namespace", |
89 | 101 | communityId: params.communityId, |
90 | 102 | namespaceId: params.namespaceId, |
91 | 103 | }, |
92 | 104 | ordering ?? "", |
93 | | - page === null ? undefined : Number(page), |
| 105 | + page, |
94 | 106 | search ?? "", |
95 | 107 | includedCategories?.split(",") ?? undefined, |
96 | 108 | excludedCategories?.split(",") ?? undefined, |
97 | 109 | section ? (section === "all" ? "" : section) : "", |
98 | | - nsfw === "true" ? true : false, |
99 | | - deprecated === "true" ? true : false |
100 | | - ), |
101 | | - }; |
| 110 | + nsfw === "true", |
| 111 | + deprecated === "true" |
| 112 | + ) |
| 113 | + .catch((error) => |
| 114 | + handleLoaderError(error, { mappings: teamNotFoundMappings }) |
| 115 | + ); |
| 116 | + const dataPromise = Promise.all([filters, listings]); |
| 117 | + |
| 118 | + return { teamId: params.namespaceId, filtersAndListings: dataPromise }; |
102 | 119 | } |
103 | | - throw new Response("Community not found", { status: 404 }); |
| 120 | + throwUserFacingPayloadResponse({ |
| 121 | + headline: "Community not found.", |
| 122 | + description: "We could not find the requested community.", |
| 123 | + category: "not_found", |
| 124 | + status: 404, |
| 125 | + }); |
104 | 126 | } |
105 | 127 |
|
| 128 | +/** |
| 129 | + * Displays the team package listing and delegates streaming data to PackageSearch. |
| 130 | + */ |
106 | 131 | export default function Team() { |
107 | | - const { filters, listings, teamId } = useLoaderData< |
108 | | - typeof loader | typeof clientLoader |
109 | | - >(); |
| 132 | + const data = useLoaderData<typeof loader | typeof clientLoader>(); |
110 | 133 |
|
111 | 134 | const outletContext = useOutletContext() as OutletContextShape; |
112 | 135 |
|
113 | 136 | return ( |
114 | | - <> |
115 | | - <section className="team"> |
116 | | - <PageHeader headingLevel="1" headingSize="3"> |
117 | | - Mods uploaded by {teamId} |
118 | | - </PageHeader> |
119 | | - <> |
120 | | - <PackageSearch |
121 | | - listings={listings} |
122 | | - filters={filters} |
123 | | - config={outletContext.requestConfig} |
124 | | - currentUser={outletContext.currentUser} |
125 | | - dapper={outletContext.dapper} |
126 | | - /> |
127 | | - </> |
128 | | - </section> |
129 | | - </> |
| 137 | + <section className="team"> |
| 138 | + <PageHeader headingLevel="1" headingSize="3"> |
| 139 | + Mods uploaded by {data.teamId} |
| 140 | + </PageHeader> |
| 141 | + <Suspense fallback={<SkeletonBox />}> |
| 142 | + <Await |
| 143 | + resolve={data.filtersAndListings} |
| 144 | + errorElement={<NimbusAwaitErrorElement />} |
| 145 | + > |
| 146 | + {([filters, listings]) => { |
| 147 | + return ( |
| 148 | + <PackageSearch |
| 149 | + listings={listings} |
| 150 | + filters={filters} |
| 151 | + config={outletContext.requestConfig} |
| 152 | + currentUser={outletContext.currentUser} |
| 153 | + dapper={outletContext.dapper} |
| 154 | + /> |
| 155 | + ); |
| 156 | + }} |
| 157 | + </Await> |
| 158 | + </Suspense> |
| 159 | + </section> |
130 | 160 | ); |
131 | 161 | } |
| 162 | + |
| 163 | +export function ErrorBoundary() { |
| 164 | + return <NimbusDefaultRouteErrorBoundary />; |
| 165 | +} |
0 commit comments