diff --git a/src/acts/rate-application.act.ts b/src/acts/rate-application.act.ts new file mode 100644 index 000000000..cf882f3c9 --- /dev/null +++ b/src/acts/rate-application.act.ts @@ -0,0 +1,23 @@ +import { parseError } from 'api-4markdown'; +import type { API4MarkdownPayload } from 'api-4markdown-contracts'; +import type { AsyncResult } from 'development-kit/utility-types'; +import { mock } from 'development-kit/mock'; + +const rateApplicationAct = async ( + payload: API4MarkdownPayload<'rateApplication'>, +): AsyncResult => { + try { + await mock({ + delay: 1, + errorFactor: 20, + error: () => new Error(`Failed to rate application`), + })({ success: true })(payload); + + return { is: `ok` }; + } catch (rawError: unknown) { + const error = parseError(rawError); + return { is: `fail`, error }; + } +}; + +export { rateApplicationAct }; diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index f9908297d..c88740066 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -18,6 +18,14 @@ type Contract = { payload: TPayload; }; +type RateApplicationContract = Contract< + `rateApplication`, + null, + { + rating: number; + description: string; + } +>; type ReportBugContract = Contract< `reportBug`, null, @@ -207,6 +215,7 @@ type API4MarkdownContracts = | UpdateMindmapContract | GetAccessibleMindmapContract | ReportBugContract + | RateApplicationContract | GetPermanentMindmapsContract; type API4MarkdownContractKey = API4MarkdownContracts['key']; diff --git a/src/containers/rate-application.container.tsx b/src/containers/rate-application.container.tsx new file mode 100644 index 000000000..605dc38e1 --- /dev/null +++ b/src/containers/rate-application.container.tsx @@ -0,0 +1,206 @@ +import React, { type FormEventHandler } from 'react'; +import { Button } from 'design-system/button'; +import { context } from 'development-kit/context'; +import { MdOutlineFeedback } from 'react-icons/md'; +import { useForm } from 'development-kit/use-form'; +import { useSimpleFeature } from '@greenonsoftware/react-kit'; +import type { Transaction } from 'development-kit/utility-types'; +import type { API4MarkdownPayload } from 'api-4markdown-contracts'; +import { Modal } from 'design-system/modal'; +import { Hint } from 'design-system/hint'; +import { rateApplicationAct } from 'acts/rate-application.act'; +import { Field } from 'design-system/field'; +import { Input } from 'design-system/input'; +import { Status } from 'design-system/status'; +import debounce from 'lodash.debounce'; + +import { + min, + max, + maxLength, + minLength, +} from 'development-kit/form/validators'; + +type FormValues = Pick< + API4MarkdownPayload<`rateApplication`>, + `rating` | `description` +>; + +const limits = { + rating: { + min: 1, + max: 5, + }, + description: { + min: 15, + max: 150, + }, +}; + +const formCfg = { + initialValue: { + rating: 0, + description: ``, + }, + validators: { + rating: [min(limits.rating.min), max(limits.rating.max)], + description: [ + minLength(limits.description.min), + maxLength(limits.description.max), + ], + }, +}; + +const [RateApplicationProvider, useRateApplicationContext] = + context(useSimpleFeature); + +const closeModal = debounce((off: () => void) => { + off(); +}, 1000); + +const RateApplicationModalContainer = () => { + const [operation, setOperation] = React.useState({ is: `idle` }); + const rateApplicationCtx = useRateApplicationContext(); + const [{ invalid, untouched, values }, { inject }] = useForm( + formCfg.initialValue, + formCfg.validators, + ); + + React.useEffect(() => { + return () => { + closeModal.cancel(); + }; + }, []); + + const ratingRange = [1, 2, 3, 4, 5]; + + const confirmSubmit: FormEventHandler = async (e) => { + e.preventDefault(); + + setOperation({ is: `busy` }); + + const result = await rateApplicationAct({ + rating: values.rating, + description: values.description, + }); + + setOperation(result); + + if (result.is === `ok`) { + closeModal(rateApplicationCtx.off); + } + }; + + const handleRatingChange = (rating: number) => { + inject(`rating`).onChange({ + target: { value: String(rating) }, + } as React.ChangeEvent); + }; + + const busy = operation.is === `busy`; + + return ( + <> + + +
+ +
+ {ratingRange.map((rating) => ( + + ))} +
+
+ + Optional, {limits.description.min}-{limits.description.max} + {` `} + characters + + } + /> + } + > + + + {operation.is === `fail` && ( +

+ {operation.error.message} +

+ )} +
+ + +
+
+
+ {operation.is === `ok` && Thank you for your feedback!} + + ); +}; + +const RateApplicationContainer = () => { + const rateApplicationCtx = useRateApplicationContext(); + return ( + <> + + {rateApplicationCtx.isOn && } + + ); +}; + +const ConnectedRateApplicationContainer = () => ( + + + +); + +export { ConnectedRateApplicationContainer as RateApplicationContainer }; diff --git a/src/features/creator/creator.view.tsx b/src/features/creator/creator.view.tsx index 6e02efa43..e179371d6 100644 --- a/src/features/creator/creator.view.tsx +++ b/src/features/creator/creator.view.tsx @@ -36,6 +36,7 @@ import { isInvalidSelection, getSelectedText, } from 'development-kit/textarea-utils'; +import { RateApplicationContainer } from 'containers/rate-application.container'; const CreatorErrorModalContainer = React.lazy( () => import(`./containers/creator-error-modal.container`), @@ -317,6 +318,7 @@ const CreatorView = () => { > {resetConfirm.isOn ? `Sure?` : `Reset`} +