diff --git a/src/app/[locale]/incident/components/IncidentFormFooter.tsx b/src/app/[locale]/incident/components/IncidentFormFooter.tsx index 36ac3f8..57b1267 100644 --- a/src/app/[locale]/incident/components/IncidentFormFooter.tsx +++ b/src/app/[locale]/incident/components/IncidentFormFooter.tsx @@ -6,14 +6,19 @@ import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/Button' import { useStepperStore } from '@/store/stepper_store' import { steps, usePathname as usePath, useRouter } from '@/routing/navigation' +import { ImSpinner8 } from 'react-icons/im' type IncidentFormFooterProps = { handleSignalSubmit?: () => void + loading?: boolean + ariaDescribedById?: string } & React.HTMLAttributes const IncidentFormFooter = ({ className, handleSignalSubmit, + loading, + ariaDescribedById, }: IncidentFormFooterProps) => { const t = useTranslations('general.describe_form') const { step, addOneStep, removeOneStep } = useStepperStore() @@ -56,12 +61,17 @@ const IncidentFormFooter = ({ )} {step === 4 && ( + // Note: current button has no visual indicator when disabled. )} diff --git a/src/app/[locale]/incident/summary/components/IncidentSummaryForm.tsx b/src/app/[locale]/incident/summary/components/IncidentSummaryForm.tsx index 49c4869..b5fed32 100644 --- a/src/app/[locale]/incident/summary/components/IncidentSummaryForm.tsx +++ b/src/app/[locale]/incident/summary/components/IncidentSummaryForm.tsx @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl' import { Divider } from '@/components/ui/Divider' import { LinkWrapper } from '@/components/ui/LinkWrapper' import { useStepperStore } from '@/store/stepper_store' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { LocationMap } from '@/components/ui/LocationMap' import { signalsClient } from '@/services/client/api-client' import { useRouter } from '@/routing/navigation' @@ -13,14 +13,16 @@ import { postAttachments } from '@/services/attachment/attachments' import { useFormStore } from '@/store/form_store' import { _NestedLocationModel } from '@/services/client' import { Paragraph, Heading } from '@/components/index' -import { MAX_NUMBER_FILES } from '@/components/ui/upload/FileUpload' import PreviewFile from '@/components/ui/upload/PreviewFile' +import { SubmitAlert } from '@/app/[locale]/incident/summary/components/SubmitAlert' const IncidentSummaryForm = () => { const t = useTranslations('describe-summary') const { formState } = useFormStore() const { goToStep } = useStepperStore() const router = useRouter() + const [error, setError] = useState(false) + const [loading, setLoading] = useState(false) useEffect(() => { router.prefetch('/incident/thankyou') @@ -36,8 +38,11 @@ const IncidentSummaryForm = () => { * generated types with the real API requirements. */ const handleSignalSubmit = async () => { - await signalsClient.v1 - .v1PublicSignalsCreate({ + setError(false) + setLoading(true) + + try { + const res = await signalsClient.v1.v1PublicSignalsCreate({ text: formState.description, // @ts-ignore location: { @@ -62,21 +67,33 @@ const IncidentSummaryForm = () => { incident_date_start: new Date().toISOString(), extra_properties: formState.extra_properties, }) - .then((res) => { - if (formState.attachments.length > 0) { - const signalId = res.signal_id - if (signalId) { - formState.attachments.forEach((attachment) => { - const formData = new FormData() - formData.append('signal_id', signalId) - formData.append('file', attachment) - postAttachments(signalId, formData) - }) + + if (formState.attachments.length > 0) { + const signalId = res.signal_id + if (signalId) { + try { + await Promise.all( + formState.attachments.map(async (attachment) => { + const formData = new FormData() + formData.append('signal_id', signalId) + formData.append('file', attachment) + return postAttachments(signalId, formData) + }) + ) + } catch (e) { + // Note: the report does not have fail when one or more of the attachments is not uploaded successfully. + console.error('One of the attachments failed while uploading', e) } } - }) - .then((res) => router.push('/incident/thankyou')) - .catch((err) => console.error(err)) + } + + router.push('/incident/thankyou') + } catch (err) { + console.error(err) + setError(true) + } finally { + setLoading(false) + } } return ( @@ -163,7 +180,14 @@ const IncidentSummaryForm = () => { )} - + + + + ) } diff --git a/src/app/[locale]/incident/summary/components/SubmitAlert.tsx b/src/app/[locale]/incident/summary/components/SubmitAlert.tsx new file mode 100644 index 0000000..2efa92d --- /dev/null +++ b/src/app/[locale]/incident/summary/components/SubmitAlert.tsx @@ -0,0 +1,50 @@ +import { useTranslations } from 'next-intl' +import React, { useEffect, useRef } from 'react' +import { + Alert, + Paragraph, +} from '@utrecht/component-library-react/dist/css-module' +import { Heading, MultilineData } from '@utrecht/component-library-react' + +export const SubmitAlert = ({ + error, + loading, +}: { + error: boolean + loading: boolean +}) => { + const t = useTranslations('describe-summary') + const alertRef = useRef(null) + const multilineRef = useRef(null) + + // Scroll to error message when an error occurs + useEffect(() => { + if ((error || loading) && alertRef.current) { + alertRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + }, [error, loading]) + + if (loading) { + return ( + + {t('submit_alert.loading.heading')} + + {t('submit_alert.loading.description')} + + + ) + } + + if (error) { + return ( + + {t('submit_alert.error.heading')} + + {t('submit_alert.error.description')} + + + ) + } + + return null +} diff --git a/src/services/attachment/attachments.ts b/src/services/attachment/attachments.ts index 77dd834..db53419 100644 --- a/src/services/attachment/attachments.ts +++ b/src/services/attachment/attachments.ts @@ -16,6 +16,6 @@ export const postAttachments = async ( return response.data } catch (error) { - throw new Error('Could not fetch suggested addresses. Please try again.') + throw new Error('Something went wrong uploading the attachment') } } diff --git a/translations/en.json b/translations/en.json index 2567e37..aefef84 100644 --- a/translations/en.json +++ b/translations/en.json @@ -78,6 +78,16 @@ "input_sharing_heading": "Report sharing", "input_sharing_allowed": "Yes, I authorize the Municipality of Amsterdam to forward my report to other organizations if it is not intended for the municipality." } + }, + "submit_alert": { + "error": { + "heading": "Submit failed", + "description": "Due to a technical error, the report could not be sent. We did not receive your information. Please try again in a few minutes." + }, + "loading": { + "heading": "Processing", + "description": "Your report is being processed. You will automatically go to the next page once the report is received.\n\nPlease wait a moment...." + } } }, "describe-thankyou": { diff --git a/translations/nl.json b/translations/nl.json index 7e157ca..54d98ab 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -78,6 +78,17 @@ "input_sharing_heading": "Melding delen", "input_sharing_allowed": "Ja, ik geef de gemeente Amsterdam toestemming om mijn melding door te sturen naar andere organisaties als de melding niet voor de gemeente is bestemd." } + }, + "submit_alert": { + "error": { + "heading": "Versturen mislukt", + "description": "Wegens een technische fout kon de melding niet worden verzonden. We hebben uw gegevens niet ontvangen. Probeer het over een paar minuten opnieuw." + + }, + "loading": { + "heading": "Bezig met afhandelen", + "description": "Uw melding wordt afgehandeld. U gaat automatisch naar de volgende pagina zodra de melding is ontvangen.\n\nEen ogenblik geduld alstublieft..." + } } }, "describe-thankyou": {