From fcd2380b9770125dc18fa16a718ab918cd662a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 10 Oct 2025 13:24:56 +0300 Subject: [PATCH 1/5] Extract package reporting from PackageListing.tsx The parts regarding opening the modal are discarded as they're outdated and wouldn't work anymore. --- .../ReportPackage/ReportPackage.css | 31 ++++ .../ReportPackage/ReportPackage.tsx | 158 ++++++++++++++++ .../cyberstorm-remix/app/p/packageListing.css | 30 --- .../cyberstorm-remix/app/p/packageListing.tsx | 172 ------------------ 4 files changed, 189 insertions(+), 202 deletions(-) create mode 100644 apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css create mode 100644 apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css new file mode 100644 index 000000000..2898030e8 --- /dev/null +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css @@ -0,0 +1,31 @@ +@layer nimbus-layout { + .report-package__body { + align-items: stretch; + } + + .report-package__block { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .report-package__label { + color: var(--color-text-primary); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-body-lg); + font-style: normal; + line-height: var(--line-height-md); + } + + .report-package__textarea { + > textarea { + width: 100%; + height: 10rem; + } + } + + .report-package__footer { + justify-content: flex-end; + } +} diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx new file mode 100644 index 000000000..8bc6b59bf --- /dev/null +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx @@ -0,0 +1,158 @@ +import { useReducer } from "react"; + +import { + NewButton, + NewSelect, + NewTextInput, + type SelectOption, + useToast, +} from "@thunderstore/cyberstorm"; +import { + type RequestConfig, + type PackageListingReportRequestData, + packageListingReport, +} from "@thunderstore/thunderstore-api"; + +import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; +import "./ReportPackage.css"; + +const reportOptions: SelectOption< + | "Spam" + | "Malware" + | "Reupload" + | "CopyrightOrLicense" + | "WrongCommunity" + | "WrongCategories" + | "Other" +>[] = [ + { value: "Spam", label: "Spam" }, + { value: "Malware", label: "Malware" }, + { value: "Reupload", label: "Reupload" }, + { value: "CopyrightOrLicense", label: "Copyright Or License" }, + { value: "WrongCommunity", label: "Wrong Community" }, + { value: "WrongCategories", label: "Wrong Categories" }, + { value: "Other", label: "Other" }, +]; + +export function ReportPackageForm(props: { + // communityId: string; + // namespaceId: string; + // packageId: string; + id: string; + config: () => RequestConfig; + toast: ReturnType; +}) { + const { + // communityId, + // namespaceId, + // packageId, + id, + toast, + config, + } = props; + + function formFieldUpdateAction( + state: PackageListingReportRequestData, + action: { + field: keyof PackageListingReportRequestData; + value: PackageListingReportRequestData[keyof PackageListingReportRequestData]; + } + ) { + return { + ...state, + [action.field]: action.value, + }; + } + + const [formInputs, updateFormFieldState] = useReducer(formFieldUpdateAction, { + reason: "Other", + description: "", + }); + + type SubmitorOutput = Awaited>; + + async function submitor(data: typeof formInputs): Promise { + return await packageListingReport({ + config: config, + params: { id: id }, + queryParams: {}, + data: { reason: data.reason, description: data.description }, + }); + } + + type InputErrors = { + [key in keyof typeof formInputs]?: string | string[]; + }; + + const strongForm = useStrongForm< + typeof formInputs, + PackageListingReportRequestData, + Error, + SubmitorOutput, + Error, + InputErrors + >({ + inputs: formInputs, + submitor, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: `Package reported`, + duration: 4000, + }); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( +
+
Report Package
+
+
+

Reason

+ { + updateFormFieldState({ field: "reason", value: value }); + }} + id="reason" + csSize="small" + /> +
+
+

+ Additional information (optional) +

+ { + updateFormFieldState({ + field: "description", + value: e.target.value, + }); + }} + placeholder="Invalid submission" + csSize="textarea" + rootClasses="report-package__textarea" + /> +
+
+
+ + Submit + +
+
+ ); +} + +ReportPackageForm.displayName = "ReportPackageForm"; diff --git a/apps/cyberstorm-remix/app/p/packageListing.css b/apps/cyberstorm-remix/app/p/packageListing.css index 2e9ccdfb6..5f932c86f 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.css +++ b/apps/cyberstorm-remix/app/p/packageListing.css @@ -76,36 +76,6 @@ justify-content: space-between; } - .report-package__body { - align-items: stretch; - } - - .report-package__block { - display: flex; - flex-direction: column; - gap: 1rem; - align-items: flex-start; - } - - .report-package__label { - color: var(--color-text-primary); - font-weight: var(--font-weight-regular); - font-size: var(--font-size-body-lg); - font-style: normal; - line-height: var(--line-height-md); - } - - .report-package__textarea { - > textarea { - width: 100%; - height: 10rem; - } - } - - .report-package__footer { - justify-content: flex-end; - } - .package-listing__package-content-section { display: flex; flex: 1 0 0; diff --git a/apps/cyberstorm-remix/app/p/packageListing.tsx b/apps/cyberstorm-remix/app/p/packageListing.tsx index 3e5a0864d..ebd14a6a8 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.tsx +++ b/apps/cyberstorm-remix/app/p/packageListing.tsx @@ -4,7 +4,6 @@ import { Suspense, useEffect, useMemo, - useReducer, useRef, useState, } from "react"; @@ -35,7 +34,6 @@ import { CopyButton } from "app/commonComponents/CopyButton/CopyButton"; import { PageHeader } from "app/commonComponents/PageHeader/PageHeader"; import TeamMembers from "app/p/components/TeamMembers/TeamMembers"; import { type OutletContextShape } from "app/root"; -import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; import { isPromise } from "cyberstorm/utils/typeChecks"; import { getPublicEnvVariables, @@ -50,11 +48,9 @@ import { NewButton, NewIcon, NewLink, - NewSelect, NewTag, NewTextInput, RelativeTime, - type SelectOption, SkeletonBox, Tabs, ThunderstoreLogo, @@ -71,8 +67,6 @@ import { fetchPackagePermissions, packageListingApprove, packageListingReject, - packageListingReport, - type PackageListingReportRequestData, type RequestConfig, } from "@thunderstore/thunderstore-api"; import { ApiAction } from "@thunderstore/ts-api-react-actions"; @@ -375,19 +369,6 @@ export default function PackageListing() { )} - - {/* Report modal is here, so that it can be reused in both desktop on mobile */} - {/* - - */}
); }); From 445789c7246ad5cd18d4a804a821d85f0fb4f336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 10 Oct 2025 13:37:38 +0300 Subject: [PATCH 2/5] Update UI to use params expected by the backend --- .../components/ReportPackage/ReportPackage.tsx | 18 +++++------------- .../src/post/packageListing.ts | 5 +---- .../src/schemas/requestSchemas.ts | 10 ++++------ 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx index 8bc6b59bf..a7bc66262 100644 --- a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx @@ -35,21 +35,13 @@ const reportOptions: SelectOption< ]; export function ReportPackageForm(props: { - // communityId: string; - // namespaceId: string; - // packageId: string; - id: string; + community: string; + namespace: string; + package: string; config: () => RequestConfig; toast: ReturnType; }) { - const { - // communityId, - // namespaceId, - // packageId, - id, - toast, - config, - } = props; + const { config, toast, ...requestParams } = props; function formFieldUpdateAction( state: PackageListingReportRequestData, @@ -74,7 +66,7 @@ export function ReportPackageForm(props: { async function submitor(data: typeof formInputs): Promise { return await packageListingReport({ config: config, - params: { id: id }, + params: requestParams, queryParams: {}, data: { reason: data.reason, description: data.description }, }); diff --git a/packages/thunderstore-api/src/post/packageListing.ts b/packages/thunderstore-api/src/post/packageListing.ts index 079b267d1..cfcf73116 100644 --- a/packages/thunderstore-api/src/post/packageListing.ts +++ b/packages/thunderstore-api/src/post/packageListing.ts @@ -108,10 +108,7 @@ export function packageListingReport( > ) { const { config, params, data } = props; - // This will most likely change to a dedicated cyberstorm API endpoint, which is commented right now. - // const path = `/api/cyberstorm/listing/${params.community}/${params.namespace}/${params.package}/report/`; - const path = `/api/experimental/package-listing/${params.id}/report/`; - + const path = `/api/cyberstorm/listing/${params.community}/${params.namespace}/${params.package}/report/`; return apiFetch({ args: { config, diff --git a/packages/thunderstore-api/src/schemas/requestSchemas.ts b/packages/thunderstore-api/src/schemas/requestSchemas.ts index 76dc83fff..b9057b1a9 100644 --- a/packages/thunderstore-api/src/schemas/requestSchemas.ts +++ b/packages/thunderstore-api/src/schemas/requestSchemas.ts @@ -450,11 +450,9 @@ export type PackageListingRejectRequestData = z.infer< // PackageListingReportRequest export const packageListingReportRequestParamsSchema = z.object({ - // This will most likely change to a dedicated cyberstorm endpoint, so the params will change to the commented ones. - id: z.string(), - // community: z.string(), - // namespace: z.string(), - // package: z.string(), + community: z.string(), + namespace: z.string(), + package: z.string(), }); export type PackageListingReportRequestParams = z.infer< @@ -462,7 +460,7 @@ export type PackageListingReportRequestParams = z.infer< >; export const packageListingReportRequestDataSchema = z.object({ - version: z.number().optional(), + version: z.number().optional(), // TODO: use SemVer string reason: z.enum([ "Spam", "Malware", From b6acad66b248260476b4e0a8268fb071a2528b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 10 Oct 2025 13:41:11 +0300 Subject: [PATCH 3/5] Avoid redefining types --- .../ReportPackage/ReportPackage.tsx | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx index a7bc66262..151119469 100644 --- a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx @@ -16,23 +16,16 @@ import { import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; import "./ReportPackage.css"; -const reportOptions: SelectOption< - | "Spam" - | "Malware" - | "Reupload" - | "CopyrightOrLicense" - | "WrongCommunity" - | "WrongCategories" - | "Other" ->[] = [ - { value: "Spam", label: "Spam" }, - { value: "Malware", label: "Malware" }, - { value: "Reupload", label: "Reupload" }, - { value: "CopyrightOrLicense", label: "Copyright Or License" }, - { value: "WrongCommunity", label: "Wrong Community" }, - { value: "WrongCategories", label: "Wrong Categories" }, - { value: "Other", label: "Other" }, -]; +const reportOptions: SelectOption[] = + [ + { value: "Spam", label: "Spam" }, + { value: "Malware", label: "Malware" }, + { value: "Reupload", label: "Reupload" }, + { value: "CopyrightOrLicense", label: "Copyright Or License" }, + { value: "WrongCommunity", label: "Wrong Community" }, + { value: "WrongCategories", label: "Wrong Categories" }, + { value: "Other", label: "Other" }, + ]; export function ReportPackageForm(props: { community: string; From ab62a1069f06e119f84cc307faa5690a5a6f53b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Mon, 13 Oct 2025 11:48:35 +0300 Subject: [PATCH 4/5] Add useReportPackageForm custom hook The idea here is to further separate the sub component from the main view component. The view component had made some optimizations that assumed everything is located in the same file. To circumvent this, small changes were required to its markup structure. Some corners were cut short on the hook's side when it comes to handling the props-as-promise. E.g. it's assumed the listing always eventually resolves to correct values, but if that doesn't happen, the whole page is broken anyway. Also no fallback is currently provided while the promise is resolving (null is returned instead, but this can be changed). useEffect is used to resolve the props rather than Suspense and Await components, as the latters caused ~1s delay for the modal to open, since when moved to hook, they started processing only when the user clicked the report button. --- .../ReportPackage/ReportPackage.css | 8 -- .../ReportPackage/ReportPackage.tsx | 83 ++++++++++++++++--- .../cyberstorm-remix/app/p/packageListing.tsx | 60 +++++++++----- 3 files changed, 112 insertions(+), 39 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css index 2898030e8..ebeab660e 100644 --- a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css @@ -1,8 +1,4 @@ @layer nimbus-layout { - .report-package__body { - align-items: stretch; - } - .report-package__block { display: flex; flex-direction: column; @@ -24,8 +20,4 @@ height: 10rem; } } - - .report-package__footer { - justify-content: flex-end; - } } diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx index 151119469..b57632958 100644 --- a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx @@ -1,7 +1,11 @@ -import { useReducer } from "react"; +import { useEffect, useReducer, useState } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFlagSwallowtail } from "@fortawesome/pro-solid-svg-icons"; import { + Modal, NewButton, + NewIcon, NewSelect, NewTextInput, type SelectOption, @@ -27,14 +31,40 @@ const reportOptions: SelectOption[] = { value: "Other", label: "Other" }, ]; -export function ReportPackageForm(props: { +function ReportPackageButton(props: { onClick: () => void }) { + return ( + + + + + + ); +} + +ReportPackageButton.displayName = "ReportPackageButton"; + +interface ReportPackageFormProps { community: string; namespace: string; package: string; config: () => RequestConfig; toast: ReturnType; -}) { - const { config, toast, ...requestParams } = props; +} + +interface ReportPackageModalProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +} + +function ReportPackageForm( + props: ReportPackageFormProps & ReportPackageModalProps +) { + const { config, toast, isOpen, setIsOpen, ...requestParams } = props; function formFieldUpdateAction( state: PackageListingReportRequestData, @@ -85,6 +115,7 @@ export function ReportPackageForm(props: { children: `Package reported`, duration: 4000, }); + setIsOpen(false); }, onSubmitError: (error) => { toast.addToast({ @@ -96,9 +127,13 @@ export function ReportPackageForm(props: { }); return ( -
-
Report Package
-
+ +

Reason

-
-
+ + Submit -
-
+ + ); } ReportPackageForm.displayName = "ReportPackageForm"; + +export function useReportPackage(formProps: Promise) { + const [isOpen, setIsOpen] = useState(false); + const [props, setProps] = useState(null); + + async function awaitAndSetProps() { + if (!props) { + setProps(await formProps); + } + } + + useEffect(() => { + awaitAndSetProps(); + }, [formProps, props]); + + const button = setIsOpen(true)} />; + + const form = props && ( + + ); + + return { + ReportPackageButton: button, + ReportPackageForm: form, + }; +} diff --git a/apps/cyberstorm-remix/app/p/packageListing.tsx b/apps/cyberstorm-remix/app/p/packageListing.tsx index ebd14a6a8..0e5d5b0aa 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.tsx +++ b/apps/cyberstorm-remix/app/p/packageListing.tsx @@ -33,6 +33,7 @@ import { faArrowUpRight, faLips } from "@fortawesome/pro-solid-svg-icons"; import { CopyButton } from "app/commonComponents/CopyButton/CopyButton"; import { PageHeader } from "app/commonComponents/PageHeader/PageHeader"; import TeamMembers from "app/p/components/TeamMembers/TeamMembers"; +import { useReportPackage } from "app/p/components/ReportPackage/ReportPackage"; import { type OutletContextShape } from "app/root"; import { isPromise } from "cyberstorm/utils/typeChecks"; import { @@ -180,6 +181,16 @@ export default function PackageListing() { const [isLiked, setIsLiked] = useState(false); const toast = useToast(); + const { ReportPackageButton, ReportPackageForm } = useReportPackage( + Promise.resolve(listing).then((listingData) => ({ + community: listingData.community_identifier, + namespace: listingData.namespace, + package: listingData.name, + config, + toast, + })) + ); + const fetchAndSetRatedPackages = async () => { const rp = await dapper.getRatedPackages(); if (isPromise(listing)) { @@ -430,6 +441,8 @@ export default function PackageListing() { )} + + {ReportPackageButton}
- - } - > - - {(resolvedValue) => ( - - )} - - +
+ + } + > + + {(resolvedValue) => ( + + )} + + + + {ReportPackageButton} +
+ @@ -622,6 +640,8 @@ export default function PackageListing() {
+ + {ReportPackageForm} ); } @@ -963,7 +983,7 @@ const Actions = memo(function Actions(props: { requestConfig, } = props; return ( -
+ <> -
+ ); }); From 668b921e63c1629705fab279a1578fe7c6a39ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Mon, 13 Oct 2025 12:37:58 +0300 Subject: [PATCH 5/5] Adjust ReportPackage form styles I'm not entirely sure how modal widths are supposed to be adjusted, or if they're supposed to just take the space required by the content. Currently the "Additional information (optional)" label sets the width for the modal, which means the textarea on the form is very narrow on desktop, making annoying to write anything longer than a couple of words. On the other hand setting a min width to the textarea makes it not that responsive on mobile layouts. I think this should be addressed in some generic fashion, perhaps in the Modal component, and is therefore outside the scope of these changes. --- .../app/p/components/ReportPackage/ReportPackage.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css index ebeab660e..538779a17 100644 --- a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css @@ -4,6 +4,7 @@ flex-direction: column; gap: 1rem; align-items: flex-start; + align-self: stretch; } .report-package__label {