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..538779a17 --- /dev/null +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.css @@ -0,0 +1,24 @@ +@layer nimbus-layout { + .report-package__block { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + align-self: stretch; + } + + .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; + } + } +} 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..b57632958 --- /dev/null +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackage.tsx @@ -0,0 +1,204 @@ +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, + 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[] = + [ + { 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" }, + ]; + +function ReportPackageButton(props: { onClick: () => void }) { + return ( + + + + + + ); +} + +ReportPackageButton.displayName = "ReportPackageButton"; + +interface ReportPackageFormProps { + community: string; + namespace: string; + package: string; + config: () => RequestConfig; + toast: ReturnType; +} + +interface ReportPackageModalProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +} + +function ReportPackageForm( + props: ReportPackageFormProps & ReportPackageModalProps +) { + const { config, toast, isOpen, setIsOpen, ...requestParams } = 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: requestParams, + 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, + }); + setIsOpen(false); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( + + +
+

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"; + +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.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..0e5d5b0aa 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"; @@ -34,8 +33,8 @@ 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 { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; import { isPromise } from "cyberstorm/utils/typeChecks"; import { getPublicEnvVariables, @@ -50,11 +49,9 @@ import { NewButton, NewIcon, NewLink, - NewSelect, NewTag, NewTextInput, RelativeTime, - type SelectOption, SkeletonBox, Tabs, ThunderstoreLogo, @@ -71,8 +68,6 @@ import { fetchPackagePermissions, packageListingApprove, packageListingReject, - packageListingReport, - type PackageListingReportRequestData, type RequestConfig, } from "@thunderstore/thunderstore-api"; import { ApiAction } from "@thunderstore/ts-api-react-actions"; @@ -186,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)) { @@ -375,19 +380,6 @@ export default function PackageListing() { )} - - {/* Report modal is here, so that it can be reused in both desktop on mobile */} - {/* - - */}
- - } - > - - {(resolvedValue) => ( - - )} - - +
+ + } + > + + {(resolvedValue) => ( + + )} + + + + {ReportPackageButton} +
+ @@ -641,6 +640,8 @@ export default function PackageListing() {
+ + {ReportPackageForm} ); } @@ -799,146 +800,6 @@ function ReviewPackageForm(props: { ReviewPackageForm.displayName = "ReviewPackageForm"; -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" }, -]; - -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="role" - /> -
-
-

- Additional information (optional) -

- { - updateFormFieldState({ - field: "description", - value: e.target.value, - }); - }} - placeholder="Invalid submission" - csSize="textarea" - rootClasses="report-package__textarea" - /> -
-
-
- - Submit - -
-
- ); -} - -ReportPackageForm.displayName = "ReportPackageForm"; - function packageTags( listing: Awaited>, community: Awaited> @@ -1122,7 +983,7 @@ const Actions = memo(function Actions(props: { requestConfig, } = props; return ( -
+ <> - {/* - - - - */} -
+ ); }); 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",