diff --git a/apps/cyberstorm-remix/app/c/Community.css b/apps/cyberstorm-remix/app/c/Community.css index 5190fb42a..1d5d40e0d 100644 --- a/apps/cyberstorm-remix/app/c/Community.css +++ b/apps/cyberstorm-remix/app/c/Community.css @@ -8,34 +8,148 @@ gap: 1rem; align-items: flex-start; align-self: stretch; - padding: 7.5rem 3rem 2rem; + padding: 1rem 3rem 2rem; + } + + .community__header { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + + /* min-height: 6.25rem; */ } .community__background { - position: absolute; display: flex; flex-direction: column; + align-items: center; + align-self: stretch; + justify-content: center; + max-height: 12.5rem; + border-radius: 0.5rem; + overflow-y: hidden; + transition: height 2s; + + > img { + border-radius: 0.5rem; + background: var(--color-ui-surface-1); + opacity: 0.8; + } + + .skeleton { + height: 12.5rem; + } + } + + .community__background--packagePage { + height: 6.25rem; + + > img { + opacity: 0.3; + mix-blend-mode: luminosity; + } + } + + .community__content-header-wrapper { + z-index: 1; + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + place-items: flex-end stretch; + align-self: stretch; + justify-content: space-between; + height: max-content; + margin-top: -1rem; + padding-left: 1rem; + transition: + height ease 1s, + opacity 0.2s, + visibility 1s 0s; + } + + .community__content-header { + display: flex; + flex-grow: 1; + gap: 1.5rem; + align-items: flex-end; + align-self: stretch; + min-width: 75%; + height: max-content; + } + + .community__content-header--hide { + height: 0; + visibility: collapse; + opacity: 0; + transition: + height ease 1s, + opacity 0.8s 0.2s, + visibility 1s 0s; + } + + .community__game-icon { + display: flex; + gap: 0.5rem; + align-items: center; + width: 7rem; + height: 7rem; + padding: var(--Space-12, 0.75rem); + border: 1px solid var(--Color-border-bright, rgb(70 70 149 / 0.66)); + border-radius: var(--Radius-xl, 1rem); + background: var(--Color-Surface-1, #070721); + aspect-ratio: 1/1; + } + + .community__game-icon-tinified { + display: flex; + flex: 1 0 0; + align-items: center; + justify-content: center; + height: 5.5rem; + + > img { + width: 5.5rem; + height: 5.5rem; + } + } + + .community__content-header-content { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: 0.75rem; align-items: flex-start; - height: 20rem; - border-radius: var(--section-border-radius); - overflow: hidden; + justify-content: center; } - .community__background-image { + .community__header-info { display: flex; + flex: 1 0 0; flex-direction: column; + gap: 0.25rem; align-items: flex-start; - background: var(--color-body-bg-color, #101028); + align-self: stretch; + min-width: 60%; + max-width: 100%; - opacity: 0.4; - mix-blend-mode: luminosity; + > h1 { + line-height: 80%; + overflow-wrap: anywhere; + } } - .community__background-tint { - position: absolute; - width: 100%; - height: 20rem; - background: linear-gradient(180deg, rgb(16 16 40 / 0.4) 0%, #101028 85.94%); + .community__header-meta { + display: flex; + flex: 0 1 60%; + gap: 1.5rem; + align-items: center; + min-width: 60%; + height: 16px; + + > .skeleton { + height: 1rem; + } } .community__small-image { @@ -62,7 +176,7 @@ .community__meta { display: flex; flex-wrap: wrap; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: center; } @@ -80,9 +194,36 @@ } } + @media (width >= 41rem) { + .community__background { + height: 12.5rem; + } + } + + @media (width < 41rem) { + .community__background { + .skeleton { + height: 8rem; + } + } + + .community__header { + gap: 1rem; + } + + .community__content-header-wrapper { + margin-top: 0; + padding-left: 0; + } + + .community__game-icon { + display: none; + } + } + @media (width <= 48rem) { .community__meta { - gap: var(--gap-xxs); + gap: var(--gap-2xs); } .community__item { diff --git a/apps/cyberstorm-remix/app/c/community.tsx b/apps/cyberstorm-remix/app/c/community.tsx index a418f6f61..c89b611a4 100644 --- a/apps/cyberstorm-remix/app/c/community.tsx +++ b/apps/cyberstorm-remix/app/c/community.tsx @@ -1,28 +1,36 @@ -import type { LoaderFunctionArgs } from "react-router"; -import { Await, useLoaderData, useOutletContext } from "react-router"; +import type { + LoaderFunctionArgs, + ShouldRevalidateFunctionArgs, +} from "react-router"; import { - NewBreadCrumbs, - NewBreadCrumbsLink, + Await, + Outlet, + useLoaderData, + useLocation, + useOutletContext, +} from "react-router"; +import { + Heading, + NewButton, NewIcon, NewLink, + SkeletonBox, } from "@thunderstore/cyberstorm"; import "./Community.css"; -import { PackageSearch } from "~/commonComponents/PackageSearch/PackageSearch"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faBook } from "@fortawesome/free-solid-svg-icons"; +import { faBook, faDownload } from "@fortawesome/free-solid-svg-icons"; import { faDiscord } from "@fortawesome/free-brands-svg-icons"; -import { PackageOrderOptions } from "~/commonComponents/PackageSearch/components/PackageOrder"; import { faArrowUpRight } from "@fortawesome/pro-solid-svg-icons"; import { DapperTs } from "@thunderstore/dapper-ts"; import { OutletContextShape } from "../root"; -import { PageHeader } from "~/commonComponents/PageHeader/PageHeader"; import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; -import { Suspense, useMemo } from "react"; +import { Suspense } from "react"; +import { classnames } from "@thunderstore/cyberstorm/src/utils/utils"; -export async function loader({ request, params }: LoaderFunctionArgs) { +export async function loader({ params }: LoaderFunctionArgs) { if (params.communityId) { const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); const dapper = new DapperTs(() => { @@ -31,42 +39,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { sessionId: undefined, }; }); - const searchParams = new URL(request.url).searchParams; - const ordering = - searchParams.get("ordering") ?? PackageOrderOptions.Updated; - const page = searchParams.get("page"); - const search = searchParams.get("search"); - const includedCategories = searchParams.get("includedCategories"); - const excludedCategories = searchParams.get("excludedCategories"); - const section = searchParams.get("section"); - const nsfw = searchParams.get("nsfw"); - const deprecated = searchParams.get("deprecated"); - const community = dapper.getCommunity(params.communityId); - const filters = dapper.getCommunityFilters(params.communityId); - + const community = await dapper.getCommunity(params.communityId); return { community: community, - filters: filters, - listings: dapper.getPackageListings( - { - kind: "community", - communityId: params.communityId, - }, - ordering ?? "", - page === null ? undefined : Number(page), - search ?? "", - includedCategories?.split(",") ?? undefined, - excludedCategories?.split(",") ?? undefined, - section ? (section === "all" ? "" : section) : "", - nsfw === "true" ? true : false, - deprecated === "true" ? true : false - ), }; } throw new Response("Community not found", { status: 404 }); } -export async function clientLoader({ request, params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { if (params.communityId) { const tools = getSessionTools(); const dapper = new DapperTs(() => { @@ -75,217 +56,203 @@ export async function clientLoader({ request, params }: LoaderFunctionArgs) { sessionId: tools?.getConfig().sessionId, }; }); - const searchParams = new URL(request.url).searchParams; - const ordering = - searchParams.get("ordering") ?? PackageOrderOptions.Updated; - const page = searchParams.get("page"); - const search = searchParams.get("search"); - const includedCategories = searchParams.get("includedCategories"); - const excludedCategories = searchParams.get("excludedCategories"); - const section = searchParams.get("section"); - const nsfw = searchParams.get("nsfw"); - const deprecated = searchParams.get("deprecated"); const community = dapper.getCommunity(params.communityId); - const filters = dapper.getCommunityFilters(params.communityId); return { community: community, - filters: filters, - listings: dapper.getPackageListings( - { - kind: "community", - communityId: params.communityId, - }, - ordering ?? "", - page === null ? undefined : Number(page), - search ?? "", - includedCategories?.split(",") ?? undefined, - excludedCategories?.split(",") ?? undefined, - section ? (section === "all" ? "" : section) : "", - nsfw === "true" ? true : false, - deprecated === "true" ? true : false - ), }; } throw new Response("Community not found", { status: 404 }); } +export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { + if ( + arg.currentUrl.pathname.split("/")[1] === arg.nextUrl.pathname.split("/")[1] + ) { + return false; + } + return arg.defaultShouldRevalidate; +} + export default function Community() { - const { community, filters, listings } = useLoaderData< - typeof loader | typeof clientLoader - >(); + const { community } = useLoaderData(); + const location = useLocation(); + const splitPath = location.pathname.split("/"); + const isSubPath = splitPath.length > 4; + const isPackageListingSubPath = + splitPath.length > 5 && splitPath[1] === "c" && splitPath[3] === "p"; const outletContext = useOutletContext() as OutletContextShape; - const listingsAndFiltersMemo = useMemo( - () => Promise.all([listings, filters]), - [] - ); - return ( <> - - - {(resolvedValue) => ( - <> - - - - - - - - - - - - )} - - - - Loading... - - } - > - - {(resolvedValue) => ( -
- {resolvedValue.hero_image_url ? ( - {resolvedValue.name} - ) : null} -
-
- )} - - -
- - - Communities - - - Loading... - - } - > - - {(resolvedValue) => ( - - {resolvedValue.name} - - )} - - - - - Loading... - - } - > + {isSubPath ? null : ( + - {(resolvedValue) => ( - - {resolvedValue.wiki_url ? ( - - - - - Modding Wiki - - - - - ) : null} - {resolvedValue.discord_url ? ( - - - - - Modding Discord - - - - - ) : null} - - } - > - {resolvedValue.name} - - )} - - - - Loading... - - } - > - {(resolvedValue) => ( <> - + + + + + + + + + )} -
+ )} + + <> +
+
+ }> + + {(resolvedValue) => + resolvedValue.hero_image_url ? ( + {resolvedValue.name} + ) : null + } + + +
+ +
+
+
+
+ }> + + {(resolvedValue) => + resolvedValue.community_icon_url ? ( + {resolvedValue.name} + ) : null + } + + +
+
+
+
+ }> + + {(resolvedValue) => ( + + {resolvedValue.name} + + )} + + +
+
+ }> + + {(resolvedValue) => + resolvedValue.wiki_url ? ( + + + + + Modding Wiki + + + + + ) : null + } + + + }> + + {(resolvedValue) => + resolvedValue.discord_url ? ( + + + + + Modding Discord + + + + + ) : null + } + + +
+
+
+ + + + + Upload package + +
+
+ + ); } diff --git a/apps/cyberstorm-remix/app/c/tabs/PackageSearch/PackageSearch.tsx b/apps/cyberstorm-remix/app/c/tabs/PackageSearch/PackageSearch.tsx new file mode 100644 index 000000000..71b1b0a0c --- /dev/null +++ b/apps/cyberstorm-remix/app/c/tabs/PackageSearch/PackageSearch.tsx @@ -0,0 +1,120 @@ +import { useLoaderData, useOutletContext } from "react-router"; +import { PackageSearch } from "~/commonComponents/PackageSearch/PackageSearch"; +import { PackageOrderOptions } from "~/commonComponents/PackageSearch/components/PackageOrder"; +import { DapperTs } from "@thunderstore/dapper-ts"; +import { + getPublicEnvVariables, + getSessionTools, +} from "cyberstorm/security/publicEnvVariables"; +import { OutletContextShape } from "~/root"; +import { Route } from "./+types/PackageSearch"; + +export async function loader({ params, request }: Route.LoaderArgs) { + if (params.communityId) { + const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); + const dapper = new DapperTs(() => { + return { + apiHost: publicEnvVariables.VITE_API_URL, + sessionId: undefined, + }; + }); + const searchParams = new URL(request.url).searchParams; + const ordering = + searchParams.get("ordering") ?? PackageOrderOptions.Updated; + const page = searchParams.get("page"); + const search = searchParams.get("search"); + const includedCategories = searchParams.get("includedCategories"); + const excludedCategories = searchParams.get("excludedCategories"); + const section = searchParams.get("section"); + const nsfw = searchParams.get("nsfw"); + const deprecated = searchParams.get("deprecated"); + const filters = await dapper.getCommunityFilters(params.communityId); + + return { + filters: filters, + listings: await dapper.getPackageListings( + { + kind: "community", + communityId: params.communityId, + }, + ordering ?? "", + page === null ? undefined : Number(page), + search ?? "", + includedCategories?.split(",") ?? undefined, + excludedCategories?.split(",") ?? undefined, + section ? (section === "all" ? "" : section) : "", + nsfw === "true" ? true : false, + deprecated === "true" ? true : false + ), + }; + } + throw new Response("Community not found", { status: 404 }); +} + +export async function clientLoader({ + request, + params, +}: Route.ClientLoaderArgs) { + if (params.communityId) { + const tools = getSessionTools(); + const dapper = new DapperTs(() => { + return { + apiHost: tools?.getConfig().apiHost, + sessionId: tools?.getConfig().sessionId, + }; + }); + const searchParams = new URL(request.url).searchParams; + const ordering = + searchParams.get("ordering") ?? PackageOrderOptions.Updated; + const page = searchParams.get("page"); + const search = searchParams.get("search"); + const includedCategories = searchParams.get("includedCategories"); + const excludedCategories = searchParams.get("excludedCategories"); + const section = searchParams.get("section"); + const nsfw = searchParams.get("nsfw"); + const deprecated = searchParams.get("deprecated"); + const filters = dapper.getCommunityFilters(params.communityId); + return { + filters: filters, + listings: dapper.getPackageListings( + { + kind: "community", + communityId: params.communityId, + }, + ordering ?? "", + page === null ? undefined : Number(page), + search ?? "", + includedCategories?.split(",") ?? undefined, + excludedCategories?.split(",") ?? undefined, + section ? (section === "all" ? "" : section) : "", + nsfw === "true" ? true : false, + deprecated === "true" ? true : false + ), + }; + } + throw new Response("Community not found", { status: 404 }); +} + +// function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { +// return true; // false +// } + +export default function CommunityPackageSearch() { + const { filters, listings } = useLoaderData< + typeof loader | typeof clientLoader + >(); + + const outletContext = useOutletContext() as OutletContextShape; + + return ( + <> + + + ); +} diff --git a/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.css b/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.css index 25af9390e..f5f80bba8 100644 --- a/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.css +++ b/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.css @@ -16,30 +16,49 @@ transition: var(--animation-duration-xs); user-select: none; + > span { + display: inline-flex; + flex-wrap: nowrap; + gap: 0.75rem; + align-items: center; + } + &.checkbox-list__label--off { color: var(--color-text-secondary); svg { --icon-color: var(--color-surface-a8); } + + .checkbox-list__exclude-button svg { + --icon-color: transparent; + } } &.checkbox-list__label--include { color: var(--color-cyber-green-7); font-weight: var(--font-weight-bold); - svg { + .checkbox-list__checkbox-button svg { --icon-color: var(--color-cyber-green-7); } + + .checkbox-list__exclude-button svg { + --icon-color: transparent; + } } &.checkbox-list__label--exclude { color: var(--color-accent-red-7); font-weight: var(--font-weight-bold); - svg { + .checkbox-list__exclude-button svg { --icon-color: var(--color-accent-red-7); } + + .checkbox-list__checkbox-button svg { + --icon-color: var(--color-surface-a8); + } } &:hover { @@ -57,22 +76,30 @@ &.checkbox-list__label--include { color: var(--color-cyber-green-8); - svg { + .checkbox-list__checkbox-button svg { --icon-color: var(--color-cyber-green-8); } + + .checkbox-list__exclude-button svg { + --icon-color: var(--color-accent-red-8); + } } &.checkbox-list__label--exclude { color: var(--color-accent-red-8); - svg { + .checkbox-list__checkbox-button svg { + --icon-color: var(--color-surface-a10); + } + + .checkbox-list__exclude-button svg { --icon-color: var(--color-accent-red-8); } } } } - .checkbox-list__checkbox { + .checkbox-list__icon { display: flex; align-items: center; justify-content: center; @@ -83,6 +110,17 @@ background-color: transparent; transition: ease-out var(--animation-duration-xs); + + &.checkbox-list__exclude-button { + --icon-inline-size: var(--space-16); + + width: var(--space-20); + height: var(--space-20); + + &:hover { + --icon-color: var(--color-accent-red-8); + } + } } } } diff --git a/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.tsx b/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.tsx index 1e7ef018d..468b8e062 100644 --- a/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.tsx +++ b/apps/cyberstorm-remix/app/commonComponents/CheckboxList/CheckboxList.tsx @@ -1,12 +1,12 @@ import { + faBan, faSquare, faSquareCheck, - faSquareXmark, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import "./CheckboxList.css"; -import { CycleButton, NewIcon } from "@thunderstore/cyberstorm"; +import { Actionable, NewIcon } from "@thunderstore/cyberstorm"; import { classnames } from "@thunderstore/cyberstorm/src/utils/utils"; import { resolveTriState } from "~/commonComponents/utils"; import { TRISTATE } from "~/commonComponents/types"; @@ -45,28 +45,48 @@ export const CheckboxList = memo(function CheckboxList(props: Props) { "checkbox-list__label", `checkbox-list__label--${cs}` )} + htmlFor={`checkbox-list__checkbox__${item.label}`} > - {item.label} - { - item.setStateFunc(nextStateResolve(item.state)); - }} - rootClasses="checkbox-list__checkbox" - value={`checkbox-list__label--${cs}`} - noState - > - - + { + if (typeof item.state === "string") { + item.setStateFunc( + item.state !== "include" ? "include" : "off" + ); + } else { + item.setStateFunc(!item.state); } - /> - - + }} + rootClasses="checkbox-list__icon checkbox-list__checkbox-button" + id={`checkbox-list__checkbox__${item.label}`} + > + + + + + {item.label} + + {typeof item.state === "string" && ( + { + if (item.state === "exclude") { + item.setStateFunc("off"); + } else { + item.setStateFunc("exclude"); + } + }} + rootClasses="checkbox-list__icon checkbox-list__exclude-button" + > + + + + + )} ); diff --git a/apps/cyberstorm-remix/app/commonComponents/Collapsible/Collapsible.css b/apps/cyberstorm-remix/app/commonComponents/Collapsible/Collapsible.css index 7ede99188..1099eb8d4 100644 --- a/apps/cyberstorm-remix/app/commonComponents/Collapsible/Collapsible.css +++ b/apps/cyberstorm-remix/app/commonComponents/Collapsible/Collapsible.css @@ -3,12 +3,17 @@ display: flex; flex: 1; flex-direction: column; + align-items: stretch; + padding: 0.5rem; + border-radius: var(--Radius-md, 0.5rem); font-size: var(--font-size-body-md); + background: var(--Color-UI-surface-1, rgb(57 57 106 / 0.15)); } .collapsible__header { display: flex; gap: var(--gap-md); + align-self: stretch; justify-content: space-between; padding: var(--space-8) var(--space-12); font-weight: var(--font-weight-bold); diff --git a/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.css b/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.css index 982599319..2496be7ba 100644 --- a/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.css +++ b/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.css @@ -1,7 +1,7 @@ @layer nimbus-components { .connection { display: flex; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: center; align-self: stretch; padding: var(--space-16) var(--space-32); @@ -48,7 +48,7 @@ .connection__actions { display: flex; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: center; justify-content: flex-end; } @@ -56,7 +56,7 @@ .connection__description { display: flex; flex-direction: column; - gap: var(--gap-xxxs); + gap: var(--gap-3xs); align-items: flex-end; justify-content: center; } diff --git a/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css b/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css index dc80f1e21..718fd0109 100644 --- a/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css +++ b/apps/cyberstorm-remix/app/commonComponents/Footer/Footer.css @@ -21,7 +21,7 @@ .footer__links-wrapper { display: flex; flex-wrap: wrap; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); justify-content: space-between; margin-left: 3.5rem; } @@ -47,7 +47,7 @@ .footer__icon-links { display: flex; flex-shrink: 0; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); justify-content: center; } @@ -181,7 +181,7 @@ .footnote__inner { display: flex; flex-grow: 1; - gap: var(--gap-xxl); + gap: var(--gap-2xl); max-width: calc(var(--footer-half-width) * 2); margin-right: 3.5rem; margin-left: 3.5rem; @@ -191,7 +191,7 @@ display: flex; flex: 1 0 0; flex-wrap: wrap; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: flex-start; > a { @@ -240,7 +240,7 @@ display: flex; flex-flow: column wrap; flex-shrink: 0; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); justify-content: space-between; max-width: unset; margin-left: unset; diff --git a/apps/cyberstorm-remix/app/commonComponents/ListingDependency/ListingDependency.css b/apps/cyberstorm-remix/app/commonComponents/ListingDependency/ListingDependency.css index b8e2e5b3f..787069f08 100644 --- a/apps/cyberstorm-remix/app/commonComponents/ListingDependency/ListingDependency.css +++ b/apps/cyberstorm-remix/app/commonComponents/ListingDependency/ListingDependency.css @@ -29,7 +29,7 @@ .listing-dependency__title { display: flex; - gap: var(--gap-xxs); + gap: var(--gap-2xs); align-items: center; align-self: stretch; font-weight: var(--font-weight-regular); diff --git a/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.css b/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.css index 7ab1b8927..31218c261 100644 --- a/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.css +++ b/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.css @@ -11,14 +11,13 @@ top: calc(var(--header-height) + 1rem); display: flex; flex-direction: column; - gap: var(--gap-xs); + gap: var(--gap-sm); align-items: flex-start; - width: 17rem; + min-width: 15.5rem; max-height: calc(100vh - var(--header-height) - 2rem); - padding: var(--space-12); + padding-top: var(--space-16); border-radius: var(--radius-md); overflow-y: auto; - background: var(--color-surface-default); scrollbar-width: none; } @@ -29,9 +28,11 @@ .package-search__filters { display: flex; flex-direction: column; - gap: var(--gap-xs); + gap: var(--gap-sm); + gap: 0.75rem; align-items: stretch; align-self: stretch; + width: 15.5rem; } .package-search__content { @@ -40,15 +41,22 @@ flex-direction: column; gap: var(--space-24); align-items: flex-start; + padding-top: 1rem; } .package-search__pagination { display: flex; - gap: var(--gap-xxs); + gap: var(--gap-2xs); align-items: center; align-self: stretch; justify-content: center; padding-top: var(--space-40); + + > .skeleton { + width: 100%; + max-width: 20rem; + height: 2.5rem; + } } .package-search__search-params { @@ -78,18 +86,10 @@ align-self: stretch; } - .package-search__results { - display: flex; - flex: 1; - gap: var(--gap-xxs); - align-items: center; - } - .package-search__listing-actions { display: flex; gap: var(--gap-md); align-items: center; - justify-content: flex-end; /* > .__display { display: flex; @@ -105,6 +105,19 @@ justify-content: space-between; } + .package-search__results { + display: flex; + flex: 1; + gap: var(--gap-2xs); + align-items: center; + justify-content: flex-end; + + .skeleton { + width: 18ch; + height: 1rem; + } + } + .package-search__packages { display: flex; flex: 1 1 0; @@ -119,6 +132,10 @@ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: var(--gap-sm); width: 100%; + + > .skeleton { + height: 30rem; + } } @media (width <= 48rem) { @@ -144,6 +161,7 @@ @media (width <= 475px) { .package-search { flex-direction: column; + gap: 0.25rem; } .package-search__sidebar { @@ -152,8 +170,13 @@ width: 100%; } + .package-search__filters { + width: unset; + } + .package-search__tools { flex-direction: column; + gap: 0.5rem; align-items: stretch; } @@ -164,9 +187,14 @@ .package-search__grid { grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + + > .skeleton { + height: 28rem; + } } .package-search__content { + gap: 0.5rem; align-self: stretch; } } diff --git a/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx b/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx index 4ed9f7db1..c063cd317 100644 --- a/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx +++ b/apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx @@ -2,11 +2,10 @@ import { faGhost, faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CurrentUser, - PackageCategory, PackageListings, Section, } from "@thunderstore/dapper/types"; -import { useEffect, useRef, useState } from "react"; +import { memo, Suspense, useEffect, useRef, useState } from "react"; import { useDebounce } from "use-debounce"; import "./PackageSearch.css"; @@ -17,12 +16,9 @@ import { NewButton, NewPagination, NewTextInput, + SkeletonBox, } from "@thunderstore/cyberstorm"; -import { - useNavigation, - useNavigationType, - useSearchParams, -} from "react-router"; +import { Await, useNavigationType, useSearchParams } from "react-router"; import { PackageCount } from "./components/PackageCount/PackageCount"; import { isPackageOrderOptions, @@ -34,17 +30,19 @@ import { RadioGroup } from "../RadioGroup/RadioGroup"; import { CategoryTagCloud } from "./components/CategoryTagCloud/CategoryTagCloud"; import { CollapsibleMenu } from "../Collapsible/Collapsible"; import { CheckboxList } from "../CheckboxList/CheckboxList"; -import { StalenessIndicator } from "../StalenessIndicator/StalenessIndicator"; import { PackageLikeAction } from "@thunderstore/cyberstorm-forms"; -import { RequestConfig } from "@thunderstore/thunderstore-api"; +import { + CommunityFilters, + RequestConfig, +} from "@thunderstore/thunderstore-api"; import { DapperTs } from "@thunderstore/dapper-ts"; +import { isPromise } from "cyberstorm/utils/typeChecks"; const PER_PAGE = 20; interface Props { - listings: PackageListings; - packageCategories: PackageCategory[]; - sections: Section[]; + listings: Promise | PackageListings; + filters: Promise | CommunityFilters; config: () => RequestConfig; currentUser?: CurrentUser; dapper: DapperTs; @@ -63,7 +61,7 @@ type SearchParamsType = { const searchParamsToBlob = ( searchParams: URLSearchParams, - sections: Section[] + sections?: Section[] ) => { const initialSearch = searchParams.getAll("search").join(" "); const initialOrder = searchParams.get("ordering"); @@ -80,7 +78,11 @@ const searchParamsToBlob = ( initialOrder && isPackageOrderOptions(initialOrder) ? (initialOrder as PackageOrderOptionsType) : undefined, - section: sections.length === 0 ? "" : initialSection ?? sections[0]?.uuid, + section: sections + ? sections.length === 0 + ? "" + : initialSection ?? sections[0]?.uuid + : initialSection ?? "", deprecated: initialDeprecated === null ? false @@ -111,10 +113,11 @@ const searchParamsToBlob = ( }; function parseCategories( - categories: CategorySelection[], includedCategories: string, - excludedCategories: string + excludedCategories: string, + categories?: CategorySelection[] ): CategorySelection[] { + if (!categories) return []; const iCArr = includedCategories.split(","); const eCArr = excludedCategories.split(","); return categories.map((c) => @@ -145,20 +148,24 @@ const compareSearchParamBlobs = ( * Component for filtering and rendering a PackageList */ export function PackageSearch(props: Props) { - const { - listings, - packageCategories: allCategories, - sections, - config, - currentUser, - dapper, - } = props; + const { listings, filters, config, currentUser, dapper } = props; + + const navigationType = useNavigationType(); - const sortedSections = sections.sort((a, b) => b.priority - a.priority); + // This exists to resolve insert the initial sections and categories on server-side + // so that we don't have to await the clientLoader to get the options, to then be able to + // do the initial fetch + const possibleFilters = isPromise(filters) ? undefined : filters; - const navigation = useNavigation(); + const [sortedSections, setSortedSections] = useState< + CommunityFilters["sections"] | undefined + >(possibleFilters?.sections); - const navigationType = useNavigationType(); + const [categories, setCategories] = useState( + possibleFilters?.package_categories + .sort((a, b) => a.slug.localeCompare(b.slug)) + .map((c) => ({ ...c, selection: "off" })) + ); const [searchParams, setSearchParams] = useSearchParams(); @@ -167,92 +174,53 @@ export function PackageSearch(props: Props) { const [searchParamsBlob, setSearchParamsBlob] = useState(initialParams); - const [currentPage, setCurrentPage] = useState(initialParams.page); - - // Start setters - const setSearch = (v: string) => { - setSearchParamsBlob({ ...searchParamsBlob, search: v }); - }; - - const setSection = (v: string) => { - setSearchParamsBlob({ ...searchParamsBlob, section: v }); - }; - - const setDeprecated = (v: boolean) => { - setSearchParamsBlob({ ...searchParamsBlob, deprecated: v }); - }; - - const setNsfw = (v: boolean) => { - setSearchParamsBlob({ ...searchParamsBlob, nsfw: v }); - }; - - const setPage = (v: number) => { - setSearchParamsBlob({ ...searchParamsBlob, page: v }); - }; - - const setOrder = (v: PackageOrderOptionsType) => { - setSearchParamsBlob({ ...searchParamsBlob, order: v }); - }; + const [currentPage, setCurrentPage] = useState( + searchParams.get("page") ? Number(searchParams.get("page")) : 1 + ); - const resetParams = (order: PackageOrderOptionsType | undefined) => { - setSearchParamsBlob({ - search: "", - order: order, - section: sortedSections.length === 0 ? "" : sortedSections[0]?.uuid, - deprecated: false, - nsfw: false, - page: 1, - includedCategories: "", - excludedCategories: "", - }); - // setOrdering(order); - // setPage(1); - // setSearchValue(""); - }; + const categoriesRef = useRef< + undefined | Awaited>["package_categories"] + >(undefined); - const clearAll = () => - setSearchParamsBlob({ - ...searchParamsBlob, - search: "", - includedCategories: "", - excludedCategories: "", - }); - // End setters + useEffect(() => { + if (isPromise(filters)) { + // On mount, resolve filters promise and set sections and categories states + filters.then((resolvedFilters) => { + // Set sorted sections + setSortedSections( + resolvedFilters.sections.sort((a, b) => b.priority - a.priority) + ); + if (sortedSections && sortedSections.length !== 0) { + setSearchParamsBlob((prev) => ({ + ...prev, + section: sortedSections[0].uuid, + })); + } + if (resolvedFilters.package_categories !== categoriesRef.current) { + // Set current "initial" categories + const categories: CategorySelection[] = + resolvedFilters.package_categories + .sort((a, b) => a.slug.localeCompare(b.slug)) + .map((c) => ({ ...c, selection: "off" })); + setCategories(categories); + categoriesRef.current = resolvedFilters.package_categories; + } + }); + } + }, []); // Categories start - const categories: CategorySelection[] = allCategories - .sort((a, b) => a.slug.localeCompare(b.slug)) - .map((c) => ({ ...c, selection: "off" })); - - const setCategories = (v: CategorySelection[]) => { - const newSearchParams = { ...searchParamsBlob }; - const includedCategories = v - .filter((c) => c.selection === "include") - .map((c) => c.id); - if (includedCategories.length === 0) { - newSearchParams.includedCategories = ""; - } else { - newSearchParams.includedCategories = includedCategories.join(","); - } - const excludedCategories = v - .filter((c) => c.selection === "exclude") - .map((c) => c.id); - if (excludedCategories.length === 0) { - newSearchParams.excludedCategories = ""; - } else { - newSearchParams.excludedCategories = excludedCategories.join(","); - } - setSearchParamsBlob({ ...newSearchParams }); - }; const parsedCategories = parseCategories( - categories, searchParamsBlob.includedCategories, - searchParamsBlob.excludedCategories + searchParamsBlob.excludedCategories, + categories ); const updateCatSelection = (catId: string, v: TRISTATE) => { - setCategories( + setParamsBlobCategories( + setSearchParamsBlob, + searchParamsBlob, parsedCategories.map((uc) => { if (uc.id === catId) { return { @@ -364,33 +332,30 @@ export function PackageSearch(props: Props) { resetPage = true; } // Section - // Because of the first section being a empty value, the logic check is a bit funky - - // If no section in search params, delete - if (sortedSections.length === 0) searchParams.delete("section"); - - // If new section is empty, delete (defaults to first) - if (debouncedSearchParamsBlob.section === "") + if ( + debouncedSearchParamsBlob.section === "" || + (sortedSections && sortedSections.length === 0) + ) { searchParams.delete("section"); - - // If new section is the first one, delete. And reset page number if section is different from last render. - if (debouncedSearchParamsBlob.section === sortedSections[0]?.uuid) { - if ( - searchParamsBlobRef.current.section !== - debouncedSearchParamsBlob.section - ) { - resetPage = true; + } else { + if (sortedSections && sortedSections.length !== 0) { + if (debouncedSearchParamsBlob.section === sortedSections[0]?.uuid) { + // If the first one, ensure the search param isn't set as it's defaulted to the first one in SearchParmsToBlob function. + searchParams.delete("section"); + } else { + searchParams.set("section", debouncedSearchParamsBlob.section); + } + } else { + // This else is for completeness + searchParams.delete("section"); } - searchParams.delete("section"); } - // If new section is different and not the first one, set it. + // Reset page if section has changed if ( searchParamsBlobRef.current.section !== - debouncedSearchParamsBlob.section && - debouncedSearchParamsBlob.section !== sortedSections[0]?.uuid + debouncedSearchParamsBlob.section ) { - searchParams.set("section", debouncedSearchParamsBlob.section); resetPage = true; } @@ -485,8 +450,8 @@ export function PackageSearch(props: Props) { } }, [debouncedSearchParamsBlob]); + // WHOLE LIKE THING const [ratedPackages, setRatedPackages] = useState([]); - const fetchAndSetRatedPackages = async () => { setRatedPackages((await dapper.getRatedPackages()).rated_packages); }; @@ -499,31 +464,18 @@ export function PackageSearch(props: Props) { } }, [currentUser]); - // End updating page - - // Start actions const likeAction = PackageLikeAction({ isLoggedIn: Boolean(currentUser?.username), dataUpdateTrigger: fetchAndSetRatedPackages, config: config, }); - // End actions + // WHOLE LIKE THING return (
- setSearch(e.target.value)} - clearValue={() => setSearch("")} - leftIcon={} - id="searchInput" - type="search" - rootClasses="package-search__search" - />
- {sortedSections.length > 0 ? ( + {sortedSections && sortedSections.length > 0 ? ( ) : null} - {categories.length > 0 ? ( + {categories && categories.length > 0 ? ( @@ -551,7 +511,11 @@ export function PackageSearch(props: Props) { { state: searchParamsBlob.deprecated, setStateFunc: (v: boolean | TRISTATE) => - setDeprecated( + setParamsBlobValue( + setSearchParamsBlob, + searchParamsBlob, + "deprecated" + )( typeof v === "boolean" ? v : v === "include" @@ -563,7 +527,11 @@ export function PackageSearch(props: Props) { { state: searchParamsBlob.nsfw, setStateFunc: (v: boolean | TRISTATE) => - setNsfw( + setParamsBlobValue( + setSearchParamsBlob, + searchParamsBlob, + "nsfw" + )( typeof v === "boolean" ? v : v === "include" @@ -579,105 +547,171 @@ export function PackageSearch(props: Props) {
+ + setParamsBlobValue( + setSearchParamsBlob, + searchParamsBlob, + "search" + )(e.target.value) + } + clearValue={() => + setParamsBlobValue( + setSearchParamsBlob, + searchParamsBlob, + "search" + )("") + } + leftIcon={} + id="searchInput" + type="search" + rootClasses="package-search__search" + />
+ setParamsBlobCategories(setSearchParamsBlob, searchParamsBlob, v) + } rootClasses="package-search__tags" - clearAll={clearAll} + clearAll={clearAll(setSearchParamsBlob, searchParamsBlob)} />
-
- -
- {/*
*/}
+
+ }> + + {(resolvedValue) => ( + + )} + + +
- - {listings.results.length > 0 ? ( -
- {listings.results.map((p) => ( - { - if (likeAction) { - likeAction( - ratedPackages.includes(`${p.namespace}-${p.name}`), - p.namespace, - p.name, - Boolean(currentUser?.username) - ); - } - }} - /> - ))} -
- ) : (searchParamsBlob.order !== undefined && searchParams.size > 1) || - (searchParamsBlob.order === undefined && searchParams.size > 0) ? ( - - - - -
- No results found - - Make sure all keywords are spelled correctly or try different - search parameters. - -
- resetParams(searchParamsBlob.order)} - rootClasses="no-result__button" - > - Clear all filters - -
- ) : ( - - - - -
- It's empty in here - - Be the first to upload a mod! - -
-
- )} -
+
+ }> + + {(resolvedValue) => ( + <> + {resolvedValue.results.length > 0 ? ( +
+ {resolvedValue.results.map((p) => ( + { + if (likeAction) { + likeAction( + ratedPackages.includes( + `${p.namespace}-${p.name}` + ), + p.namespace, + p.name, + Boolean(currentUser?.username) + ); + } + }} + /> + ))} +
+ ) : (searchParamsBlob.order !== undefined && + searchParams.size > 1) || + (searchParamsBlob.order === undefined && + searchParams.size > 0) ? ( + + + + +
+ No results found + + Make sure all keywords are spelled correctly or try + different search parameters. + +
+ + resetParams( + setSearchParamsBlob, + searchParamsBlob.order, + sortedSections + ) + } + rootClasses="no-result__button" + > + Clear all filters + +
+ ) : ( + + + + +
+ + It's empty in here + + + Be the first to upload a mod! + +
+
+ )} + + )} +
+
+
- + }> + + {(resolvedValue) => ( + + )} + +
@@ -685,3 +719,80 @@ export function PackageSearch(props: Props) { } PackageSearch.displayName = "PackageSearch"; + +// Start setters +function setParamsBlobValue( + setter: (v: SearchParamsType) => void, + oldBlob: SearchParamsType, + key: K +) { + return (v: SearchParamsType[K]) => setter({ ...oldBlob, [key]: v }); +} + +const setParamsBlobCategories = ( + setter: (v: SearchParamsType) => void, + oldBlob: SearchParamsType, + v: CategorySelection[] +) => { + const newSearchParams = { ...oldBlob }; + const includedCategories = v + .filter((c) => c.selection === "include") + .map((c) => c.id); + if (includedCategories.length === 0) { + newSearchParams.includedCategories = ""; + } else { + newSearchParams.includedCategories = includedCategories.join(","); + } + const excludedCategories = v + .filter((c) => c.selection === "exclude") + .map((c) => c.id); + if (excludedCategories.length === 0) { + newSearchParams.excludedCategories = ""; + } else { + newSearchParams.excludedCategories = excludedCategories.join(","); + } + setter({ ...newSearchParams }); +}; + +const resetParams = ( + setter: (v: SearchParamsType) => void, + order: PackageOrderOptionsType | undefined, + sortedSections: CommunityFilters["sections"] | undefined +) => { + setter({ + search: "", + order: order, + section: sortedSections + ? sortedSections.length === 0 + ? "" + : sortedSections[0]?.uuid + : "", + deprecated: false, + nsfw: false, + page: 1, + includedCategories: "", + excludedCategories: "", + }); +}; + +const clearAll = + (setter: (v: SearchParamsType) => void, oldBlob: SearchParamsType) => () => + setter({ + ...oldBlob, + search: "", + includedCategories: "", + excludedCategories: "", + }); +// End setters + +const PackageSearchPackagesSkeleton = memo( + function PackageSearchPackagesSkeleton() { + return ( +
+ {Array.from({ length: 12 }).map((_, index) => ( + + ))} +
+ ); + } +); diff --git a/apps/cyberstorm-remix/app/commonComponents/PackageSearch/components/PackageCount/PackageCount.css b/apps/cyberstorm-remix/app/commonComponents/PackageSearch/components/PackageCount/PackageCount.css index 1133e418e..66c29a67a 100644 --- a/apps/cyberstorm-remix/app/commonComponents/PackageSearch/components/PackageCount/PackageCount.css +++ b/apps/cyberstorm-remix/app/commonComponents/PackageSearch/components/PackageCount/PackageCount.css @@ -3,6 +3,7 @@ display: flex; flex: 1; flex-wrap: wrap; + justify-content: flex-end; color: var(--color-text-tertiary); font-size: var(--font-size-body-md); word-break: break-all; diff --git a/apps/cyberstorm-remix/app/commonComponents/PageHeader/PageHeader.css b/apps/cyberstorm-remix/app/commonComponents/PageHeader/PageHeader.css index 1836b0db0..2c3cddc72 100644 --- a/apps/cyberstorm-remix/app/commonComponents/PageHeader/PageHeader.css +++ b/apps/cyberstorm-remix/app/commonComponents/PageHeader/PageHeader.css @@ -1,7 +1,8 @@ @layer nimbus-components { .page-header { display: flex; - flex: 1 1 0; + + /* flex: 1 1 0; */ flex-direction: row; gap: var(--space-32); align-items: flex-start; diff --git a/apps/cyberstorm-remix/app/communities/Communities.css b/apps/cyberstorm-remix/app/communities/Communities.css index 165050c71..ad7868656 100644 --- a/apps/cyberstorm-remix/app/communities/Communities.css +++ b/apps/cyberstorm-remix/app/communities/Communities.css @@ -12,7 +12,7 @@ .communities__content { display: flex; flex-direction: column; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: flex-start; align-self: stretch; } @@ -38,7 +38,7 @@ display: grid; flex-flow: row wrap; grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); - gap: var(--gap-xxxxxxxl) var(--gap-xl); + gap: var(--gap-7xl) var(--gap-xl); width: 100%; } @@ -48,8 +48,8 @@ align-items: flex-start; align-self: stretch; aspect-ratio: 3/4; - border-radius: var(--radius-md); - background: var(--color-skeleton-bg-color); + + --skeleton-radius: var(--radius-md); } .communities__community-skeleton-content { @@ -65,8 +65,6 @@ align-items: flex-start; width: 90%; height: 1.188rem; - border-radius: var(--radius-sm); - background: var(--color-skeleton-bg-color); } .communities__community-skeleton-meta { @@ -75,12 +73,16 @@ align-self: stretch; justify-content: space-between; height: 0.937rem; - border-radius: var(--radius-sm); - background: var(--color-skeleton-bg-color); } } } + @media (width <= 41rem) { + .communities__communities-list { + gap: var(--gap-3xl) var(--gap-xl); + } + } + @media (width <= 39rem) { .communities__tools { flex-direction: column; diff --git a/apps/cyberstorm-remix/app/communities/communities.tsx b/apps/cyberstorm-remix/app/communities/communities.tsx index 01746f0c7..709e5f561 100644 --- a/apps/cyberstorm-remix/app/communities/communities.tsx +++ b/apps/cyberstorm-remix/app/communities/communities.tsx @@ -4,6 +4,7 @@ import { EmptyState, NewTextInput, NewSelect, + SkeletonBox, } from "@thunderstore/cyberstorm"; import "./Communities.css"; import { useState, useEffect, useRef, memo, Suspense } from "react"; @@ -78,7 +79,7 @@ export async function loader({ request }: LoaderFunctionArgs) { }; }); return { - communities: dapper.getCommunities( + communities: await dapper.getCommunities( page, order === null ? undefined : order, search === null ? undefined : search @@ -160,7 +161,7 @@ export default function CommunitiesPage() { }, [debouncedSearchValue]); return ( -
+ <> Communities @@ -174,6 +175,7 @@ export default function CommunitiesPage() { leftIcon={} type="search" rootClasses="communities__search" + csSize="small" />
- + ); } @@ -240,10 +242,16 @@ const CommunitiesListSkeleton = memo(function CommunitiesListSkeleton() {
{Array.from({ length: 14 }).map((_, index) => (
-
+
+ +
-
-
+
+ +
+
+ +
))} diff --git a/apps/cyberstorm-remix/app/p/dependants/Dependants.tsx b/apps/cyberstorm-remix/app/p/dependants/Dependants.tsx index c4b39c41a..8db343bcb 100644 --- a/apps/cyberstorm-remix/app/p/dependants/Dependants.tsx +++ b/apps/cyberstorm-remix/app/p/dependants/Dependants.tsx @@ -1,14 +1,11 @@ -import type { LoaderFunctionArgs, MetaFunction } from "react-router"; -import { useLoaderData, useOutletContext } from "react-router"; +import { Await, useLoaderData, useOutletContext } from "react-router"; import { formatToDisplayName, - NewBreadCrumbs, - NewBreadCrumbsLink, NewLink, + SkeletonBox, } from "@thunderstore/cyberstorm"; import "./Dependants.css"; import { PackageSearch } from "~/commonComponents/PackageSearch/PackageSearch"; -import { ApiError } from "@thunderstore/thunderstore-api"; import { DapperTs } from "@thunderstore/dapper-ts"; import { PackageOrderOptions } from "../../commonComponents/PackageSearch/components/PackageOrder"; import { OutletContextShape } from "../../root"; @@ -17,230 +14,160 @@ import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; +import { Route } from "./+types/Dependants"; +import { Suspense } from "react"; -export const meta: MetaFunction = ({ data }) => { - return [ - { title: data?.community.name }, - { name: "description", content: `Mods for ${data?.community.name}` }, - ]; -}; - -export async function loader({ request, params }: LoaderFunctionArgs) { - if (params.communityId && params.namespaceId && params.packageId) { - try { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - const searchParams = new URL(request.url).searchParams; - const ordering = - searchParams.get("ordering") ?? PackageOrderOptions.Updated; - const page = searchParams.get("page"); - const search = searchParams.get("search"); - const includedCategories = searchParams.get("includedCategories"); - const excludedCategories = searchParams.get("excludedCategories"); - const section = searchParams.get("section"); - const nsfw = searchParams.get("nsfw"); - const deprecated = searchParams.get("deprecated"); - const community = await dapper.getCommunity(params.communityId); - const filters = await dapper.getCommunityFilters(params.communityId); - const sortedSections = filters.sections.sort( - (a, b) => b.priority - a.priority - ); +export async function loader({ params, request }: Route.LoaderArgs) { + if (params.communityId && params.packageId && params.namespaceId) { + const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); + const dapper = new DapperTs(() => { return { - community: community, - filters: filters, - listing: await dapper.getPackageListingDetails( - params.communityId, - params.namespaceId, - params.packageId - ), - listings: await dapper.getPackageListings( - { - kind: "package-dependants", - communityId: params.communityId, - namespaceId: params.namespaceId, - packageName: params.packageId, - }, - ordering ?? "", - page === null ? undefined : Number(page), - search ?? "", - includedCategories?.split(",") ?? undefined, - excludedCategories?.split(",") ?? undefined, - section - ? section === "all" - ? "" - : section - : sortedSections && sortedSections[0] - ? sortedSections[0].uuid - : "", - nsfw === "true" ? true : false, - deprecated === "true" ? true : false - ), - sortedSections: sortedSections, + apiHost: publicEnvVariables.VITE_API_URL, + sessionId: undefined, }; - } catch (error) { - if (error instanceof ApiError) { - throw new Response("Package not found", { status: 404 }); - } else { - throw error; - } - } + }); + const searchParams = new URL(request.url).searchParams; + const ordering = + searchParams.get("ordering") ?? PackageOrderOptions.Updated; + const page = searchParams.get("page"); + const search = searchParams.get("search"); + const includedCategories = searchParams.get("includedCategories"); + const excludedCategories = searchParams.get("excludedCategories"); + const section = searchParams.get("section"); + const nsfw = searchParams.get("nsfw"); + const deprecated = searchParams.get("deprecated"); + const filters = await dapper.getCommunityFilters(params.communityId); + + return { + community: dapper.getCommunity(params.communityId), + listing: dapper.getPackageListingDetails( + params.communityId, + params.namespaceId, + params.packageId + ), + filters: filters, + listings: await dapper.getPackageListings( + { + kind: "package-dependants", + communityId: params.communityId, + namespaceId: params.namespaceId, + packageName: params.packageId, + }, + ordering ?? "", + page === null ? undefined : Number(page), + search ?? "", + includedCategories?.split(",") ?? undefined, + excludedCategories?.split(",") ?? undefined, + section ? (section === "all" ? "" : section) : "", + nsfw === "true" ? true : false, + deprecated === "true" ? true : false + ), + }; } - throw new Response("Package not found", { status: 404 }); + throw new Response("Community not found", { status: 404 }); } -export async function clientLoader({ request, params }: LoaderFunctionArgs) { - if (params.communityId && params.namespaceId && params.packageId) { - try { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); - const searchParams = new URL(request.url).searchParams; - const ordering = - searchParams.get("ordering") ?? PackageOrderOptions.Updated; - const page = searchParams.get("page"); - const search = searchParams.get("search"); - const includedCategories = searchParams.get("includedCategories"); - const excludedCategories = searchParams.get("excludedCategories"); - const section = searchParams.get("section"); - const nsfw = searchParams.get("nsfw"); - const deprecated = searchParams.get("deprecated"); - const community = await dapper.getCommunity(params.communityId); - const filters = await dapper.getCommunityFilters(params.communityId); - const sortedSections = filters.sections.sort( - (a, b) => b.priority - a.priority - ); +export async function clientLoader({ + request, + params, +}: Route.ClientLoaderArgs) { + if (params.communityId && params.packageId && params.namespaceId) { + const tools = getSessionTools(); + const dapper = new DapperTs(() => { return { - community: community, - filters: filters, - listing: await dapper.getPackageListingDetails( - params.communityId, - params.namespaceId, - params.packageId - ), - listings: await dapper.getPackageListings( - { - kind: "package-dependants", - communityId: params.communityId, - namespaceId: params.namespaceId, - packageName: params.packageId, - }, - ordering ?? "", - page === null ? undefined : Number(page), - search ?? "", - includedCategories?.split(",") ?? undefined, - excludedCategories?.split(",") ?? undefined, - section - ? section === "all" - ? "" - : section - : sortedSections && sortedSections[0] - ? sortedSections[0].uuid - : "", - nsfw === "true" ? true : false, - deprecated === "true" ? true : false - ), - sortedSections: sortedSections, + apiHost: tools?.getConfig().apiHost, + sessionId: tools?.getConfig().sessionId, }; - } catch (error) { - if (error instanceof ApiError) { - throw new Response("Package not found", { status: 404 }); - } else { - throw error; - } - } + }); + const searchParams = new URL(request.url).searchParams; + const ordering = + searchParams.get("ordering") ?? PackageOrderOptions.Updated; + const page = searchParams.get("page"); + const search = searchParams.get("search"); + const includedCategories = searchParams.get("includedCategories"); + const excludedCategories = searchParams.get("excludedCategories"); + const section = searchParams.get("section"); + const nsfw = searchParams.get("nsfw"); + const deprecated = searchParams.get("deprecated"); + const filters = dapper.getCommunityFilters(params.communityId); + return { + community: dapper.getCommunity(params.communityId), + listing: dapper.getPackageListingDetails( + params.communityId, + params.namespaceId, + params.packageId + ), + filters: filters, + listings: dapper.getPackageListings( + { + kind: "package-dependants", + communityId: params.communityId, + namespaceId: params.namespaceId, + packageName: params.packageId, + }, + ordering ?? "", + page === null ? undefined : Number(page), + search ?? "", + includedCategories?.split(",") ?? undefined, + excludedCategories?.split(",") ?? undefined, + section ? (section === "all" ? "" : section) : "", + nsfw === "true" ? true : false, + deprecated === "true" ? true : false + ), + }; } - throw new Response("Package not found", { status: 404 }); + throw new Response("Community not found", { status: 404 }); } export default function Dependants() { - const { community, filters, listing, listings, sortedSections } = - useLoaderData(); + const { filters, listing, listings } = useLoaderData< + typeof loader | typeof clientLoader + >(); const outletContext = useOutletContext() as OutletContextShape; return ( -
- - - Communities - - - {community.name} - - - {listing.namespace} - - - {formatToDisplayName(listing.name)} - - - Dependants - - + <>
- - Mods that depend on{" "} - - {formatToDisplayName(listing.name)} - - {" by "} - - {listing.namespace} - - - + }> + + {(resolvedValue) => ( + + Mods that depend on{" "} + + {formatToDisplayName(resolvedValue.name)} + + {" by "} + + {resolvedValue.namespace} + + + )} + + + <> + +
-
+ ); } diff --git a/apps/cyberstorm-remix/app/p/packageEdit.css b/apps/cyberstorm-remix/app/p/packageEdit.css index b82b0ed8a..0ba9d6f4f 100644 --- a/apps/cyberstorm-remix/app/p/packageEdit.css +++ b/apps/cyberstorm-remix/app/p/packageEdit.css @@ -1,11 +1,7 @@ @layer nimbus-layout { - .package-edit { - --nimbus-layout-content-max-width: 90rem; - } - .package-edit__main { display: flex; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: center; align-self: stretch; } diff --git a/apps/cyberstorm-remix/app/p/packageEdit.tsx b/apps/cyberstorm-remix/app/p/packageEdit.tsx index 75f66d265..a146434af 100644 --- a/apps/cyberstorm-remix/app/p/packageEdit.tsx +++ b/apps/cyberstorm-remix/app/p/packageEdit.tsx @@ -2,8 +2,6 @@ import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { useLoaderData, useOutletContext, useRevalidator } from "react-router"; import { NewAlert, - NewBreadCrumbs, - NewBreadCrumbsLink, NewButton, NewIcon, NewSelectSearch, @@ -236,46 +234,7 @@ export default function PackageListing() { }); return ( -
- - - Communities - - - {community.name} - - - {listing.namespace} - - - {listing.name} - - - Edit package - - + <> Edit package @@ -439,6 +398,6 @@ export default function PackageListing() {
-
+ ); } diff --git a/apps/cyberstorm-remix/app/p/packageListing.css b/apps/cyberstorm-remix/app/p/packageListing.css index 3cad99814..2e9ccdfb6 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.css +++ b/apps/cyberstorm-remix/app/p/packageListing.css @@ -1,40 +1,7 @@ @layer nimbus-layout { - .package-listing { - --nimbus-layout-content-max-width: 120rem; - - z-index: 1; - } - - .package-community__background { - position: absolute; - display: flex; - flex-direction: column; - align-items: flex-start; - height: 20rem; - border-radius: var(--section-border-radius); - overflow: hidden; - } - - .package-community__background-image { - display: flex; - flex-direction: column; - align-items: flex-start; - background: var(--color-body-bg-color, #101028); - - opacity: 0.4; - mix-blend-mode: luminosity; - } - - .package-community__background-tint { - position: absolute; - width: 100%; - height: 20rem; - background: linear-gradient(180deg, rgb(16 16 40 / 0.4) 0%, #101028 85.94%); - } - .package-listing__main { display: flex; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: flex-start; align-self: stretch; } @@ -49,12 +16,11 @@ align-items: flex-start; align-self: stretch; justify-content: flex-start; - padding: 7.5rem 3rem 2rem; } .package-listing__actions { position: absolute; - top: 2.5rem; + top: -5rem; right: 3rem; display: flex; flex-direction: column; @@ -151,19 +117,19 @@ scrollbar-width: none; } - .package-listing__content-header { - display: flex; - gap: var(--gap-xl); - align-items: flex-start; - align-self: stretch; - padding: 1rem; + .package-listing__page-header-skeleton { + height: 10rem; + } + + .package-listing__nav-skeleton { + height: 47px; } .package-listing__content { display: flex; flex: 1 0 0; flex-direction: column; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: flex-start; align-self: stretch; } @@ -179,6 +145,16 @@ padding-top: var(--gap-md); } + .package-listing-sidebar__skeleton { + width: 20rem; + height: 21.4rem; + } + + .package-listing-sidebar__install-skeleton { + width: 20rem; + height: 3.56rem; + } + .package-listing-sidebar__install { justify-content: center; width: 100%; @@ -192,6 +168,11 @@ align-self: stretch; } + .package-listing-sidebar__actions-skeleton { + width: 20rem; + height: 2.25rem; + } + .package-listing-sidebar__actions { display: flex; gap: var(--gap-xs); @@ -207,7 +188,7 @@ .package-listing-sidebar__meta { display: flex; flex-direction: column; - gap: var(--gap-xxxs); + gap: var(--gap-3xs); align-items: flex-start; align-self: stretch; @@ -222,7 +203,7 @@ .package-listing-sidebar__item { display: flex; - gap: var(--gap-xxxl); + gap: var(--gap-3xl); align-items: center; align-self: stretch; padding: var(--space-8) var(--space-12); @@ -267,6 +248,11 @@ word-break: break-word; } + .package-listing-sidebar__boxes-skeleton { + width: 20rem; + height: 11rem; + } + .package-listing-sidebar__categories { display: flex; flex-direction: column; @@ -308,6 +294,21 @@ width: 100%; } + @media (width < 41rem) { + .package-listing__actions { + position: unset; + top: unset; + right: unset; + display: flex; + flex: 1 1 0; + } + + .package-listing-management-tools { + flex: 1 1 0; + flex-flow: row wrap; + } + } + /* Sidebar narrow/wide switch breakpoint */ @media (width <= 990px) { .package-listing-sidebar { diff --git a/apps/cyberstorm-remix/app/p/packageListing.tsx b/apps/cyberstorm-remix/app/p/packageListing.tsx index 2173a6910..479bf843a 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.tsx +++ b/apps/cyberstorm-remix/app/p/packageListing.tsx @@ -1,4 +1,7 @@ -import type { LoaderFunctionArgs } from "react-router"; +import type { + LoaderFunctionArgs, + ShouldRevalidateFunctionArgs, +} from "react-router"; import { Await, Outlet, @@ -11,14 +14,13 @@ import { Heading, Modal, NewAlert, - NewBreadCrumbs, - NewBreadCrumbsLink, NewButton, NewIcon, NewLink, NewSelect, NewTag, NewTextInput, + SkeletonBox, Tabs, } from "@thunderstore/cyberstorm"; import "./packageListing.css"; @@ -160,6 +162,20 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { clientLoader.hydrate = true; +export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { + const oldPath = arg.currentUrl.pathname.split("/"); + const newPath = arg.nextUrl.pathname.split("/"); + // If we're staying on the same package page, don't revalidate + if ( + oldPath[2] === newPath[2] && + oldPath[4] === newPath[4] && + oldPath[5] === newPath[5] + ) { + return false; + } + return arg.defaultShouldRevalidate; +} + export default function PackageListing() { const { community, listing, team, permissions } = useLoaderData< typeof loader | typeof clientLoader @@ -296,24 +312,6 @@ export default function PackageListing() { )} - - - {(resolvedValue) => - resolvedValue.hero_image_url ? ( -
- {resolvedValue.hero_image_url ? ( - {resolvedValue.name} - ) : null} -
-
- ) : null - } - -
@@ -332,74 +330,13 @@ export default function PackageListing() { } - - - Communities - - - Loading... - - } - > - - {(resolvedValue) => ( - - {resolvedValue.name} - - )} - - - - Loading... - - } - > - - {(resolvedValue) => ( - - {resolvedValue[0].namespace} - - )} - - - - Loading... - - } - > - - {(resolvedValue) => ( - - {formatToDisplayName(resolvedValue.name)} - - )} - - -
- Loading...}> + + } + > {(resolvedValue) => ( - - Loading... - - } - > + Loading...

}> {(resolvedValue) => ( <> @@ -496,13 +427,7 @@ export default function PackageListing() { )}
- - Loading... - - } - > + Loading...

}> {(resolvedValue) => ( <> @@ -516,13 +441,7 @@ export default function PackageListing() {
- - Loading... - - } - > + Loading...

}> {(resolvedValue) => ( - Loading... - + } > @@ -653,9 +570,7 @@ export default function PackageListing() {