diff --git a/src/hooks/api/offers.ts b/src/hooks/api/offers.ts index 78ac4b1b9..b97ca0b02 100644 --- a/src/hooks/api/offers.ts +++ b/src/hooks/api/offers.ts @@ -49,7 +49,10 @@ const useGetOffersByCreatorQuery = ( advancedQuery?: string; } >, - configuration: UseQueryOptions = {}, + { + queryArguments, + ...configuration + }: UseQueryOptions & { queryArguments?: any } = {}, ) => { const defaultQuery = `creator:(${creator?.id} OR ${creator?.email})`; const query = advancedQuery @@ -70,6 +73,7 @@ const useGetOffersByCreatorQuery = ( ...createSortingArgument(sortOptions), ...(calendarSummaryFormats && createEmbededCalendarSummaries(calendarSummaryFormats)), + ...(queryArguments ?? {}), }, enabled: !!(creator?.id && creator?.email), ...configuration, diff --git a/src/hooks/api/user.ts b/src/hooks/api/user.ts index c260c7fc3..5c3b3cfa2 100644 --- a/src/hooks/api/user.ts +++ b/src/hooks/api/user.ts @@ -1,3 +1,4 @@ +import { User } from '@/types/User'; import { fetchFromApi, isErrorObject } from '@/utils/fetchFromApi'; import { diff --git a/src/i18n/de.json b/src/i18n/de.json index 35f444faf..fd1e8e54a 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -200,6 +200,11 @@ "events": "Wo findet diese Veranstaltung oder Aktivität statt?", "places": "Wo ist dieser Ort oder Ort?" }, + "recent_locations": { + "title": "Wählen Sie einen Standort aus, den Sie kürzlich verwendet haben", + "info": "Wir haben die Standorte, für die Sie kürzlich eingegeben haben, hier für Sie gespeichert. Sie können sie mit einem Klick hinzufügen.", + "other": "Oder wählen Sie einen anderen Standort" + }, "validation_messages": { "street_and_number": { "required": "Ihre Straße und Hausnummer entsprechen nicht den Anforderungen." diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 1d756db4d..9a17576ae 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -200,6 +200,11 @@ "events": "Où cet événement ou cette activité aura-t-il lieu?", "places": "Où est cet endroit ou cet emplacement?" }, + "recent_locations": { + "title": "Choisissez un lieu que vous avez utilisé récemment", + "info": "Nous avons mis les lieux pour lesquels vous avez récemment entré ici pour vous. Vous pouvez les ajouter en un clic.", + "other": "Ou choisissez un autre lieu" + }, "validation_messages": { "street_and_number": { "required": "Votre rue et votre numéro ne répondent pas aux exigences." diff --git a/src/i18n/nl.json b/src/i18n/nl.json index a286318fc..18d0919bb 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -200,6 +200,11 @@ "events": "Waar vindt dit evenement of deze activiteit plaats?", "places": "Waar is deze plaats of locatie?" }, + "recent_locations": { + "title": "Kies een locatie die je recent gebruikte", + "info": "We hebben de locaties waarvoor je recent invoerde hier voor je klaargezet. Met één klik voeg je ze toe.", + "other": "Of kies een andere locatie" + }, "validation_messages": { "street_and_number": { "required": "Je straat en nummer voldoen niet aan de vereisten." diff --git a/src/pages/steps/AdditionalInformationStep/OrganizerPicker.tsx b/src/pages/steps/AdditionalInformationStep/OrganizerPicker.tsx index ed1e373bc..1ab7b356c 100644 --- a/src/pages/steps/AdditionalInformationStep/OrganizerPicker.tsx +++ b/src/pages/steps/AdditionalInformationStep/OrganizerPicker.tsx @@ -14,6 +14,7 @@ import { Values } from '@/types/Values'; import { Alert, AlertVariants } from '@/ui/Alert'; import { Badge, BadgeVariants } from '@/ui/Badge'; import { Button, ButtonVariants } from '@/ui/Button'; +import { ButtonCard } from '@/ui/ButtonCard'; import { FormElement } from '@/ui/FormElement'; import { Icon, Icons } from '@/ui/Icon'; import { Inline } from '@/ui/Inline'; @@ -81,63 +82,17 @@ const RecentUsedOrganizers = ({ ? organizer.address[organizer.mainLanguage] : organizer.address : ''; + return ( - + badge={isUitpasOrganizer(organizer) && } + description={ + address && `${address.postalCode} ${address.addressLocality}` + } + /> ); })} diff --git a/src/pages/steps/LocationStep.tsx b/src/pages/steps/LocationStep.tsx index a4562d759..d6e3add8f 100644 --- a/src/pages/steps/LocationStep.tsx +++ b/src/pages/steps/LocationStep.tsx @@ -1,6 +1,7 @@ import { TFunction } from 'i18next'; +import { uniqBy } from 'lodash'; import getConfig from 'next/config'; -import { ChangeEvent, useEffect, useMemo, useState } from 'react'; +import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'; import { Controller, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; @@ -13,18 +14,25 @@ import { useChangeLocationMutation, useChangeOnlineUrlMutation, useDeleteOnlineUrlMutation, + useGetEventByIdQuery, } from '@/hooks/api/events'; -import { useGetEventByIdQuery } from '@/hooks/api/events'; -import { useGetPlaceByIdQuery } from '@/hooks/api/places'; -import { useChangeAddressMutation } from '@/hooks/api/places'; +import { useGetOffersByCreatorQuery } from '@/hooks/api/offers'; +import { + useChangeAddressMutation, + useGetPlaceByIdQuery, +} from '@/hooks/api/places'; +import { useGetUserQuery } from '@/hooks/api/user'; +import { SupportedLanguage } from '@/i18n/index'; import { FormData as OfferFormData } from '@/pages/create/OfferForm'; import { Address } from '@/types/Address'; import { Countries, Country } from '@/types/Country'; import { AttendanceMode, AudienceType } from '@/types/Event'; +import { Offer } from '@/types/Offer'; import { Values } from '@/types/Values'; -import { Alert } from '@/ui/Alert'; +import { Alert, AlertVariants } from '@/ui/Alert'; import { parseSpacing } from '@/ui/Box'; import { Button, ButtonVariants } from '@/ui/Button'; +import { ButtonCard } from '@/ui/ButtonCard'; import { FormElement } from '@/ui/FormElement'; import { Icon, Icons } from '@/ui/Icon'; import { Inline } from '@/ui/Inline'; @@ -34,7 +42,8 @@ import { RadioButtonTypes } from '@/ui/RadioButton'; import { RadioButtonWithLabel } from '@/ui/RadioButtonWithLabel'; import { getStackProps, Stack, StackProps } from '@/ui/Stack'; import { Text, TextVariants } from '@/ui/Text'; -import { getValueFromTheme } from '@/ui/theme'; +import { Breakpoints, getValueFromTheme } from '@/ui/theme'; +import { getLanguageObjectOrFallback } from '@/utils/getLanguageObjectOrFallback'; import { parseOfferId } from '@/utils/parseOfferId'; import { prefixUrlWithHttp } from '@/utils/url'; @@ -58,6 +67,95 @@ const API_URL = publicRuntimeConfig.apiUrl; const getGlobalValue = getValueFromTheme('global'); +const RecentLocations = ({ onFieldChange, ...props }) => { + const { t, i18n } = useTranslation(); + const getUserQuery = useGetUserQuery(); + const getOffersQuery = useGetOffersByCreatorQuery( + { + advancedQuery: '_exists_:location.id', + // @ts-expect-error + creator: getUserQuery?.data, + sortOptions: { + field: 'modified', + order: 'desc', + }, + paginationOptions: { start: 0, limit: 20 }, + }, + { + queryArguments: { + workflowStatus: 'DRAFT,READY_FOR_VALIDATION,APPROVED', + addressCountry: '*', + }, + }, + ); + + const offers: (Offer & { location: any })[] = + // @ts-expect-error + getOffersQuery?.data?.member ?? []; + const locations = uniqBy( + offers?.map((offer) => offer.location), + '@id', + ).filter((location) => location && location.name.nl !== 'Online'); + + return ( + + + + {t('create.location.recent_locations.title')} + + + + + {t('create.location.recent_locations.info')} + + + {locations.map((location) => { + const address = + location.address[location.mainLanguage] ?? location.address; + + return ( + + onFieldChange({ + municipality: { + zip: address.postalCode, + label: `${address.postalCode} ${address.addressLocality}`, + name: address.addressLocality, + }, + place: location, + }) + } + title={getLanguageObjectOrFallback( + location.name, + i18n.language as SupportedLanguage, + location?.mainLanguage ?? 'nl', + )} + description={ + address && ( + <> + {address.streetAddress} +
+ {address.postalCode} {address.addressLocality} + + ) + } + /> + ); + })} +
+
+ ); +}; + const useEditLocation = ({ scope, offerId }: UseEditArguments) => { const { i18n } = useTranslation(); const changeAddressMutation = useChangeAddressMutation(); @@ -276,6 +374,34 @@ const LocationStep = ({ const { isOnline, municipality, country } = field?.value as OfferFormData['location']; + const onFieldChange = (updatedValue) => { + updatedValue = { ...field.value, ...updatedValue }; + field.onChange(updatedValue); + onChange(updatedValue); + field.onBlur(); + }; + + const renderFieldWithRecentLocations = (children) => ( + + {!isOnline && ( + + )} + + {!isOnline && ( + + {t('create.location.recent_locations.other')} + + )} + {children} + + + ); + const OnlineToggle = ( @@ -285,15 +411,11 @@ const LocationStep = ({ type={RadioButtonTypes.SWITCH} checked={isOnline} onChange={(e) => { - const updatedValue = { - ...field.value, + onFieldChange({ place: undefined, municipality: undefined, isOnline: e.target.checked, - }; - field.onChange(updatedValue); - field.onBlur(); - onChange(updatedValue); + }); }} css={` .custom-switch { @@ -314,56 +436,60 @@ const LocationStep = ({ return ( {OnlineToggle} - { - const prefixedUrl = - e.target.value === '' - ? e.target.value - : prefixUrlWithHttp(e.target.value); - const updatedValue = { - ...field?.value, - onlineUrl: prefixedUrl, - }; - field.onChange(updatedValue); - if (isValidUrl(prefixedUrl)) { - onChange(updatedValue); - setHasOnlineUrlError(false); - } else { - setHasOnlineUrlError(true); - } - }} - onChange={(e) => { - setOnlineUrl(e.target.value); - }} - placeholder={t('create.location.online_url.placeholder')} - /> - } - id="online-url" - label={t('create.location.online_url.label')} - error={ - hasOnlineUrlError && - t('create.validation_messages.location.online_url') - } - info={ - - {t('create.location.online_url.info')} - - } - /> + {renderFieldWithRecentLocations( + { + const prefixedUrl = + e.target.value === '' + ? e.target.value + : prefixUrlWithHttp(e.target.value); + const updatedValue = { + ...field?.value, + onlineUrl: prefixedUrl, + }; + field.onChange(updatedValue); + if (isValidUrl(prefixedUrl)) { + onChange(updatedValue); + setHasOnlineUrlError(false); + } else { + setHasOnlineUrlError(true); + } + }} + onChange={(e) => { + setOnlineUrl(e.target.value); + }} + placeholder={t( + 'create.location.online_url.placeholder', + )} + /> + } + id="online-url" + label={t('create.location.online_url.label')} + error={ + hasOnlineUrlError && + t('create.validation_messages.location.online_url') + } + info={ + + {t('create.location.online_url.info')} + + } + />, + )} ); } if (!country || municipality?.zip === '0000') { - return ( - + return renderFieldWithRecentLocations( + <> { - const updatedValue = { - ...field.value, + onFieldChange({ country: Countries.BE, municipality: undefined, - }; - field.onChange(updatedValue); - onChange(updatedValue); + }); setAudienceType(AudienceType.EVERYONE); - field.onBlur(); }} > {t('create.location.country.change_location')} @@ -390,157 +512,132 @@ const LocationStep = ({ {t('create.location.country.location_school_info')} - + , ); } if (!municipality) { return ( - + <> {scope === OfferTypes.EVENTS && OnlineToggle} - - { - const updatedValue = { - ...field.value, - municipality: value, - place: undefined, - }; - field.onChange(updatedValue); - onChange(updatedValue); - field.onBlur(); - }} - width="22rem" - /> - { - const updatedValue = { - ...field.value, - country: newCountry, - }; - field.onChange(updatedValue); - onChange(updatedValue); - field.onBlur(); - }} - css={` - & button { - margin-bottom: 0.3rem; + {renderFieldWithRecentLocations( + + { + onFieldChange({ + municipality: value, + place: undefined, + }); + }} + width="22rem" + /> + + onFieldChange({ country: newCountry }) } - `} - /> - - - + css={` + & button { + margin-bottom: 0.3rem; + } + `} + /> + + , + )} + ); } - return ( - - - - - {municipality.name} - - - {scope === OfferTypes.EVENTS && ( - - )} - {scope === OfferTypes.PLACES && ( - - {field.value.streetAndNumber ? ( - - - {field.value.streetAndNumber} - - - ) : ( - { - const updatedValue = { - ...field.value, - streetAndNumber: streetAndNumber, - }; - field.onChange(updatedValue); - onChange(updatedValue); - }} - onChange={handleChangeStreetAndNumber} - /> - } - id="location-streetAndNumber" - label={t('location.add_modal.labels.streetAndNumber')} - maxWidth="28rem" - error={ - formState.errors.location?.streetAndNumber && - t('location.add_modal.errors.streetAndNumber') - } + return renderFieldWithRecentLocations( + <> + + + {municipality.name} + + + {scope === OfferTypes.EVENTS && ( + + )} + {scope === OfferTypes.PLACES && ( + + {field.value.streetAndNumber ? ( + + - )} - - )} - - + {field.value.streetAndNumber} + + + ) : ( + onFieldChange({ streetAndNumber })} + onChange={handleChangeStreetAndNumber} + /> + } + id="location-streetAndNumber" + label={t('location.add_modal.labels.streetAndNumber')} + maxWidth="28rem" + error={ + formState.errors.location?.streetAndNumber && + t('location.add_modal.errors.streetAndNumber') + } + /> + )} + + )} + , ); }} /> diff --git a/src/pages/steps/PlaceStep.tsx b/src/pages/steps/PlaceStep.tsx index c6d1b3a0e..30a90ab9b 100644 --- a/src/pages/steps/PlaceStep.tsx +++ b/src/pages/steps/PlaceStep.tsx @@ -39,6 +39,7 @@ type PlaceStepProps = StackProps & country?: Country; chooseLabel: (t: TFunction) => string; placeholderLabel: (t: TFunction) => string; + onFieldChange: StepProps['onChange']; }; const PlaceStep = ({ @@ -48,7 +49,7 @@ const PlaceStep = ({ control, name, loading, - onChange, + onFieldChange, terms, municipality, country, @@ -134,10 +135,7 @@ const PlaceStep = ({ municipality={municipality} country={country} onConfirmSuccess={(place) => { - const updatedValue = { ...field.value, place }; - field.onChange(updatedValue); - onChange(updatedValue); - field.onBlur(); + onFieldChange({ place }); setIsPlaceAddModalVisible(false); }} /> @@ -202,10 +200,7 @@ const PlaceStep = ({ return; } - const updatedValue = { ...field.value, place }; - - field.onChange(updatedValue); - onChange(updatedValue); + onFieldChange({ place }); }} minLength={3} placeholder={placeholderLabel(t)} @@ -228,8 +223,11 @@ const PlaceStep = ({ color={getGlobalValue('successIcon')} /> - {selectedPlace.name[i18n.language] ?? - selectedPlace.name[selectedPlace.mainLanguage]} + {getLanguageObjectOrFallback( + selectedPlace.name, + i18n.language as SupportedLanguage, + selectedPlace.mainLanguage ?? 'nl', + )} + ); +} + +export { ButtonCard };