diff --git a/package.json b/package.json index c134343a0..2f162237f 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "react-query": "^3.2.0", "react-router": "^5.2.0", "react-table": "^7.6.3", + "sanitize-html": "^2.13.1", "sass": "^1.27.0", "sass-loader": "^10.0.4", "socket.io-client": "2.3.0", diff --git a/src/hooks/api/authenticated-query.ts b/src/hooks/api/authenticated-query.ts index f00bbe341..cc664863e 100644 --- a/src/hooks/api/authenticated-query.ts +++ b/src/hooks/api/authenticated-query.ts @@ -171,15 +171,15 @@ const isDuplicateMutation = ( // Temporary disable caching on price-info // https://jira.uitdatabank.be/browse/III-5620 - if (mutationKey === 'offers-add-price-info') { - return false; - } - if (mutationKey === 'offers-change-calendar') { - return false; - } + const disabledMutations = [ + 'offers-add-price-info', + 'offers-change-calendar', + 'places-add', + 'request-ownership', + ]; - if (mutationKey === 'places-add') { + if (disabledMutations.includes(mutationKey)) { return false; } @@ -189,6 +189,11 @@ const isDuplicateMutation = ( const latestMutation = mutations.slice(-2)[0]; + // If the latest mutation was unsuccessful, we don't want to trigger a false positive. + if (latestMutation.state.error) { + return false; + } + return ( mutations.length > 1 && isEqual(latestMutation.options.variables, variables) ); diff --git a/src/hooks/api/organizers.ts b/src/hooks/api/organizers.ts index ae2e6f5b6..3e755c495 100644 --- a/src/hooks/api/organizers.ts +++ b/src/hooks/api/organizers.ts @@ -3,6 +3,7 @@ import type { UseMutationOptions, UseQueryOptions } from 'react-query'; import type { AuthenticatedQueryOptions, PaginationOptions, + ServerSideQueryOptions, SortOptions, } from '@/hooks/api/authenticated-query'; import { @@ -12,6 +13,7 @@ import { import type { Organizer } from '@/types/Organizer'; import { createSortingArgument } from '@/utils/createSortingArgument'; import { fetchFromApi, isErrorObject } from '@/utils/fetchFromApi'; +import { handleErrorObject } from '@/utils/handleErrorObject'; import type { Headers } from './types/Headers'; import type { User } from './user'; @@ -165,6 +167,36 @@ const getOrganizersByCreator = async ({ return await res.json(); }; +type UseGetOrganizerPermissionsArguments = ServerSideQueryOptions & { + organizerId: string; +}; + +const getOrganizerPermissions = async ({ headers, organizerId }) => { + const res = await fetchFromApi({ + path: `/organizers/${organizerId}/permissions`, + options: { headers }, + }); + + return handleErrorObject(res); +}; + +export type GetOrganizerPermissionsResponse = { + permissions: string[]; +}; +const useGetOrganizerPermissions = ( + { req, queryClient, organizerId }: UseGetOrganizerPermissionsArguments, + configuration: UseQueryOptions = {}, +) => + useAuthenticatedQuery({ + req, + queryClient, + queryKey: ['ownership-permissions'], + queryFn: getOrganizerPermissions, + queryArguments: { organizerId }, + refetchOnWindowFocus: false, + ...configuration, + }); + const deleteOrganizerById = async ({ headers, id }) => fetchFromApi({ path: `/organizers/${id}`, @@ -357,6 +389,7 @@ export { useDeleteOrganizerByIdMutation, useDeleteOrganizerEducationalDescriptionMutation, useGetOrganizerByIdQuery, + useGetOrganizerPermissions, useGetOrganizersByCreatorQuery, useGetOrganizersByQueryQuery, useGetOrganizersByWebsiteQuery, diff --git a/src/hooks/api/ownerships.ts b/src/hooks/api/ownerships.ts index 7c08e9306..228b12bf7 100644 --- a/src/hooks/api/ownerships.ts +++ b/src/hooks/api/ownerships.ts @@ -30,16 +30,17 @@ export const RequestState = { type RequestState = Values; -const requestOwnership = async ({ headers, itemId, itemType, ownerEmail }) => +const requestOwnership = async ({ headers, itemId, ownerEmail, ownerId }) => fetchFromApi({ path: `/ownerships`, options: { method: 'POST', headers, body: JSON.stringify({ - ownerEmail, itemId, - itemType, + itemType: 'organizer', + ...(ownerEmail && { ownerEmail }), + ...(ownerId && { ownerId }), }), }, }); @@ -51,11 +52,12 @@ const useRequestOwnershipMutation = (configuration: UseQueryOptions = {}) => ...configuration, }); -const getOwnershipRequests = async ({ headers, organizerId }) => { +const getOwnershipRequests = async ({ headers, organizerId, ownerId }) => { const res = await fetchFromApi({ path: '/ownerships/', searchParams: { itemId: organizerId, + ...(ownerId && { ownerId }), }, options: { headers, @@ -70,10 +72,11 @@ const getOwnershipRequests = async ({ headers, organizerId }) => { type UseGetOwnershipRequestsArguments = ServerSideQueryOptions & { organizerId: string; + ownerId?: string; }; const useGetOwnershipRequestsQuery = ( - { req, queryClient, organizerId }: UseGetOwnershipRequestsArguments, + { req, queryClient, organizerId, ownerId }: UseGetOwnershipRequestsArguments, configuration: UseQueryOptions = {}, ) => useAuthenticatedQuery({ @@ -81,7 +84,7 @@ const useGetOwnershipRequestsQuery = ( queryClient, queryKey: ['ownership-requests'], queryFn: getOwnershipRequests, - queryArguments: { organizerId }, + queryArguments: { organizerId, ...(ownerId && { ownerId }) }, refetchOnWindowFocus: false, ...configuration, }); diff --git a/src/hooks/api/user.ts b/src/hooks/api/user.ts index ead2c360c..223a8e62b 100644 --- a/src/hooks/api/user.ts +++ b/src/hooks/api/user.ts @@ -57,7 +57,7 @@ const getUser = async (cookies: Cookies) => { const useGetUserQuery = () => { const { cookies } = useCookiesWithOptions(['idToken']); - return useAuthenticatedQuery({ + return useAuthenticatedQuery({ queryKey: ['user'], queryFn: () => getUser(cookies), }); diff --git a/src/hooks/useLegacyPath.tsx b/src/hooks/useLegacyPath.tsx index cbfd4790f..e11885e8e 100644 --- a/src/hooks/useLegacyPath.tsx +++ b/src/hooks/useLegacyPath.tsx @@ -1,12 +1,14 @@ import getConfig from 'next/config'; import { useRouter } from 'next/router'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { useCookiesWithOptions } from './useCookiesWithOptions'; +import { useSearchParams } from './useSearchParams'; const useLegacyPath = () => { const { cookies } = useCookiesWithOptions(['token', 'udb-language']); const router = useRouter(); + const searchParams = useSearchParams(); const { publicRuntimeConfig } = getConfig(); const prefixWhenNotEmpty = (value, prefix) => value ? `${prefix}${value}` : value; @@ -15,24 +17,21 @@ const useLegacyPath = () => { const lang = cookies['udb-language']; const legacyPath = useMemo(() => { - const path = new URL(`http://localhost${router.asPath}`).pathname; - const { params = [], ...queryWithoutParams } = router.query; - const queryString = prefixWhenNotEmpty( - new URLSearchParams({ - ...queryWithoutParams, - jwt, - lang, - }), - '?', - ); + const path = new URL(router.asPath, publicRuntimeConfig.legacyAppUrl) + .pathname; + searchParams.set('jwt', jwt); + searchParams.set('lang', lang); - return `${publicRuntimeConfig.legacyAppUrl}${path}${queryString}`; + return `${publicRuntimeConfig.legacyAppUrl}${path}${prefixWhenNotEmpty( + searchParams.toString(), + '?', + )}`; }, [ router.asPath, - router.query, jwt, lang, publicRuntimeConfig.legacyAppUrl, + searchParams, ]); return legacyPath; diff --git a/src/hooks/useSearchParams.tsx b/src/hooks/useSearchParams.tsx new file mode 100644 index 000000000..1da466ca8 --- /dev/null +++ b/src/hooks/useSearchParams.tsx @@ -0,0 +1,25 @@ +import { useRouter } from 'next/router'; +import { useMemo } from 'react'; + +export const useSearchParams = () => { + const { query, pathname } = useRouter(); + + return useMemo(() => { + const params = new URLSearchParams(); + + const filtered = Object.entries(query).filter(([key]) => { + return !pathname.includes(`[${key}]`); + }); + + filtered.forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((v) => params.append(key, v)); + return; + } + + params.set(key, value); + }); + + return params; + }, [query, pathname]); +}; diff --git a/src/i18n/de.json b/src/i18n/de.json index 7aa75f1e9..033cf59aa 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -990,6 +990,25 @@ } }, "organizers": { + "detail": { + "name": "Name", + "description": "Beschreibung", + "educationalDescription": "Beschreibung Cultuurkuur", + "address": "Adresse", + "no_address": "Keine Adresse", + "contact": "Kontakt", + "no_contact": "Keine Kontaktinformationen", + "labels": "Labels", + "images": "Bilder", + "no_images": "Keine Bilder", + "mainImage": "Hauptbild", + "actions": { + "edit": "Bearbeiten", + "manage": "Anforderungsmanagement", + "request": "Verwaltung anfordern", + "back": "Zurück zum Dashboard" + } + }, "create": { "title": "Organisation hinzufügen", "step1": { @@ -1063,6 +1082,16 @@ "body": "Sind Sie sicher, dass Sie {{ownerEmail}} als Verwalter entfernen möchten?", "cancel": "Abbrechen", "confirm": "Verwalter entfernen" + }, + "request": { + "confirm_modal": { + "title": "Verwaltungsanfrage für {{organizerName}}", + "body": "Sind Sie im Vorstand von {{organizerName}} und möchten Sie die Organisationsseite und Veranstaltungen verwalten? Starten Sie hier Ihre Anfrage, um Administrator zu werden. Sobald ein aktueller Administrator Ihre Anfrage genehmigt, erhalten Sie Zugriff und eine Bestätigung per E-Mail.", + "cancel": "Abbrechen", + "confirm": "Verwaltungsanfrage" + }, + "pending": "Ihre Anfrage zur Verwaltung von {{organizerName}} wird bearbeitet.
Sobald ein aktueller Administrator Ihre Anfrage genehmigt hat, erhalten Sie Zugriff und eine Bestätigung per E-Mail.", + "success": "Ihre Anfrage zur Verwaltung der Seite von {{organizerName}} und ihrer Veranstaltungen wurde erfolgreich gesendet!
Sobald ein aktueller Administrator Ihre Anfrage genehmigt, erhalten Sie Zugriff und eine Bestätigung per E-Mail." } } }, diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 8da7ae856..76a57eb97 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -990,6 +990,25 @@ } }, "organizers": { + "detail": { + "name": "Nom", + "description": "Description", + "educationalDescription": "Description Cultuurkuur", + "address": "Adresse", + "no_address": "Aucune adresse", + "contact": "Contact", + "no_contact": "Aucune information de contact", + "labels": "Labels", + "images": "Images", + "no_images": "Aucune image", + "mainImage": "Image principale", + "actions": { + "edit": "Modifier", + "manage": "Gestion des demandes", + "request": "Demander gestion", + "back": "Retourner au tableau de bord" + } + }, "create": { "title": "Ajouter une organisation", "step1": { @@ -1063,6 +1082,16 @@ "body": "Êtes-vous sûr de vouloir supprimer {{ownerEmail}} en tant que gestionnaire ?", "cancel": "Annuler", "confirm": "Supprimer le gestionnaire" + }, + "request": { + "confirm_modal": { + "title": "Demande de gestion pour {{organizerName}}", + "body": "Êtes-vous impliqué dans la gestion de {{organizerName}} et souhaitez-vous gérer la page de l'organisation et de ses événements ? Commencez ici votre demande pour devenir administrateur. Une fois qu'un administrateur actuel approuve votre demande, vous recevrez l'accès et une confirmation par e-mail.", + "cancel": "Annuler", + "confirm": "Demander la gestion" + }, + "pending": "Votre demande pour gérer {{organizerName}} est en cours de traitement.
Dès qu'un administrateur actuel aura approuvé votre demande, vous recevrez l'accès et une confirmation par e-mail.", + "success": "Votre demande pour gérer la page de {{organizerName}} et ses événements a été envoyée avec succès !
Dès qu'un administrateur actuel approuve votre demande, vous recevrez l'accès et une confirmation par e-mail." } } }, diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 5669ff203..c652db81b 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -990,6 +990,25 @@ } }, "organizers": { + "detail": { + "name": "Naam", + "description": "Bescrijving", + "educationalDescription": "Beschrijving Cultuurkuur", + "address": "Adres", + "no_address": "Geen address", + "contact": "Contact", + "no_contact": "Geen contactinformatie", + "labels": "Labels", + "images": "Afbeeldingen", + "no_images": "Geen afbeeldingen", + "mainImage": "Hoofdafbeelding", + "actions": { + "edit": "Bewerken", + "manage": "Beheerders aanpassen", + "request": "Beheer aanvragen", + "back": "Terug naar dashboard" + } + }, "create": { "title": "Organisatie toevoegen", "step1": { @@ -1063,6 +1082,17 @@ "body": "Ben je zeker dat je {{ownerEmail}} wilt verwijderen als beheerder?", "cancel": "Annuleren", "confirm": "Beheerder verwijderen" + }, + "request": { + "confirm_modal": { + "title": "Beheer aanvragen voor {{organizerName}}", + "body": "Ben je betrokken bij het bestuur van {{organizerName}} en wil je de organisatiepagina en evenementen ervan beheren? Start hier je aanvraag om beheerder te worden. Zodra een huidige beheerder je aanvraag goedkeurt, krijg je toegang en ontvang je een bevestiging via e-mail.", + "cancel": "Annuleren", + "confirm": "Beheer aanvragen", + "error": "Er is iets misgelopen" + }, + "pending": "Je aanvraag tot het beheer van {{organizerName}} is in behandeling.
Zodra een huidige beheerder je aanvraag heeft goedgekeurd, krijg je toegang en ontvang je een bevestiging via e-mail.", + "success": "Je aanvraag om beheerder te worden van {{organizerName}} is succesvol verzonden!
Zodra een huidige beheerder je aanvraag heeft goedgekeurd, krijg je toegang en ontvang je een bevestiging via e-mail." } } }, diff --git a/src/middleware.api.ts b/src/middleware.api.ts index 994a67820..75e380622 100644 --- a/src/middleware.api.ts +++ b/src/middleware.api.ts @@ -44,51 +44,8 @@ export const middleware = async (request: NextRequest) => { const url = new URL('/beta-version', request.url); return NextResponse.redirect(url); } - - const isOwnershipPage = - request.nextUrl.pathname.startsWith('/organizer') && - !request.nextUrl.pathname.endsWith('/ownerships'); - - if (isOwnershipPage) { - const isOwnershipEnabled = - process.env.NEXT_PUBLIC_OWNERSHIP_ENABLED === 'true'; - - const redirectUrl = new URL(request.nextUrl); - // remove the path variables from nextjs routing - redirectUrl.searchParams.delete('params'); - - if ( - isOwnershipEnabled && - redirectUrl.searchParams.get('ownership') === 'true' - ) { - return NextResponse.next(); - } - - if ( - isOwnershipEnabled && - redirectUrl.searchParams.get('ownership') !== 'true' - ) { - redirectUrl.searchParams.set('ownership', 'true'); - return NextResponse.redirect(redirectUrl); - } - - if (!isOwnershipEnabled && redirectUrl.searchParams.has('ownership')) { - redirectUrl.searchParams.delete('ownership'); - return NextResponse.redirect(redirectUrl); - } - - return NextResponse.next(); - } }; export const config = { - matcher: [ - '/event', - '/login', - '/organizers/:id/ownerships', - '/organizer/(.*)', - '/(.*)/ownerships', - '/search(.*)', - '/[...params]', - ], + matcher: ['/event', '/login'], }; diff --git a/src/pages/CityPicker.tsx b/src/pages/CityPicker.tsx index 6f522f0cc..432c0957c 100644 --- a/src/pages/CityPicker.tsx +++ b/src/pages/CityPicker.tsx @@ -6,7 +6,7 @@ import { City, useGetCitiesByQuery } from '@/hooks/api/cities'; import { Countries, Country } from '@/types/Country'; import { FormElement } from '@/ui/FormElement'; import { getStackProps, Stack, StackProps } from '@/ui/Stack'; -import { Typeahead } from '@/ui/Typeahead'; +import { Typeahead, TypeaheadElement } from '@/ui/Typeahead'; import { valueToArray } from '@/utils/valueToArray'; import { SupportedLanguages } from '../i18n'; @@ -20,7 +20,7 @@ type Props = Omit & { error?: string; }; -const CityPicker = forwardRef( +const CityPicker = forwardRef, Props>( ( { offerId, country, name, value, onChange, onBlur, error, ...props }, ref, diff --git a/src/pages/[...params].page.js b/src/pages/[...params].page.js index 144b96841..23f930a9d 100644 --- a/src/pages/[...params].page.js +++ b/src/pages/[...params].page.js @@ -9,6 +9,7 @@ import { WindowMessageTypes, } from '@/hooks/useHandleWindowMessage'; import { useIsClient } from '@/hooks/useIsClient'; +import { useLegacyPath } from '@/hooks/useLegacyPath'; import PageNotFound from '@/pages/404.page'; import { Box } from '@/ui/Box'; import { getApplicationServerSideProps } from '@/utils/getApplicationServerSideProps'; @@ -49,30 +50,7 @@ const Fallback = () => { const isClientSide = useIsClient(); - const { cookies } = useCookiesWithOptions(['token', 'udb-language']); - const token = cookies['token']; - const language = cookies['udb-language']; - - const legacyPath = useMemo(() => { - const path = new URL(`http://localhost${router.asPath}`).pathname; - const { params = [], ...queryWithoutParams } = router.query; - const queryString = prefixWhenNotEmpty( - new URLSearchParams({ - ...queryWithoutParams, - jwt: token, - lang: language, - }), - '?', - ); - - return `${publicRuntimeConfig.legacyAppUrl}${path}${queryString}`; - }, [ - language, - publicRuntimeConfig.legacyAppUrl, - router.asPath, - router.query, - token, - ]); + const legacyPath = useLegacyPath(); if (notFoundPaths.includes(router.asPath)) { return ; diff --git a/src/pages/create/OfferForm.tsx b/src/pages/create/OfferForm.tsx index 7600cfbe9..2b242b28f 100644 --- a/src/pages/create/OfferForm.tsx +++ b/src/pages/create/OfferForm.tsx @@ -87,7 +87,7 @@ const getTerms = (typeAndTheme: FormDataUnion['typeAndTheme']) => { return { terms }; }; -const getAddress = ( +export const getAddress = ( address: Address, language: SupportedLanguage, mainLanguage: SupportedLanguage, diff --git a/src/pages/organizers/[organizerId]/ownerships/index.page.tsx b/src/pages/organizers/[organizerId]/ownerships/index.page.tsx index babcd4368..25654549f 100644 --- a/src/pages/organizers/[organizerId]/ownerships/index.page.tsx +++ b/src/pages/organizers/[organizerId]/ownerships/index.page.tsx @@ -57,10 +57,7 @@ const Ownership = () => { const translationsPath = `organizers.ownerships.${actionType}_modal`; const { register, formState, getValues, setError } = useForm(); - const organizerId = useMemo( - () => router.query.organizerId as string, - [router.query.organizerId], - ); + const organizerId = router.query.organizerId as string; const getOrganizerByIdQuery = useGetOrganizerByIdQuery({ id: organizerId, @@ -153,7 +150,6 @@ const Ownership = () => { case ActionType.REQUEST: return requestOwnership.mutate({ ownerEmail: getValues('email'), - itemType: 'organizer', itemId: parseOfferId(organizer['@id']), }); } diff --git a/src/pages/organizers/[organizerId]/preview/OrganizerLabels.tsx b/src/pages/organizers/[organizerId]/preview/OrganizerLabels.tsx new file mode 100644 index 000000000..600db6182 --- /dev/null +++ b/src/pages/organizers/[organizerId]/preview/OrganizerLabels.tsx @@ -0,0 +1,152 @@ +import { uniq } from 'lodash'; +import { useRouter } from 'next/router'; +import React, { useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { UseQueryResult } from 'react-query'; + +import { ScopeTypes } from '@/constants/OfferType'; +import { useGetLabelsByQuery } from '@/hooks/api/labels'; +import { + useAddOfferLabelMutation, + useRemoveOfferLabelMutation, +} from '@/hooks/api/offers'; +import { LABEL_PATTERN } from '@/pages/steps/AdditionalInformationStep/LabelsStep'; +import { Label, Offer } from '@/types/Offer'; +import { Organizer } from '@/types/Organizer'; +import { Alert } from '@/ui/Alert'; +import { FormElement } from '@/ui/FormElement'; +import { Icon, Icons } from '@/ui/Icon'; +import { Inline } from '@/ui/Inline'; +import { Stack } from '@/ui/Stack'; +import { Text, TextVariants } from '@/ui/Text'; +import { getGlobalBorderRadius, getValueFromTheme } from '@/ui/theme'; +import { Typeahead, TypeaheadElement } from '@/ui/Typeahead'; +import { getUniqueLabels } from '@/utils/getUniqueLabels'; + +type OrganizerLabelProps = { + organizer: Organizer; +}; + +export const OrganizerLabelsForm = ({ organizer }: OrganizerLabelProps) => { + const { t } = useTranslation(); + const ref = useRef>(null); + const router = useRouter(); + const [query, setQuery] = useState(''); + // @ts-expect-error + const labelsQuery: UseQueryResult<{ member: Label[] }> = useGetLabelsByQuery({ + query, + }); + + const scope = ScopeTypes.ORGANIZERS; + const organizerId = router.query.organizerId as string; + + const options = labelsQuery.data?.member ?? []; + const [labels, setLabels] = useState( + getUniqueLabels(organizer) ?? [], + ); + const addLabelMutation = useAddOfferLabelMutation(); + const removeLabelMutation = useRemoveOfferLabelMutation(); + const getButtonValue = getValueFromTheme('button'); + + const isWriting = addLabelMutation.isLoading || removeLabelMutation.isLoading; + const [isInvalid, setIsInvalid] = useState(false); + + return ( + + + + {t('create.additionalInformation.labels.info')} + + } + loading={isWriting} + error={ + isInvalid + ? t('create.additionalInformation.labels.error') + : undefined + } + Component={ + { + const label = newLabels[0]?.name; + if (!label || !label.match(LABEL_PATTERN)) { + return setIsInvalid(true); + } + + setIsInvalid(false); + await addLabelMutation.mutateAsync({ + id: organizerId, + scope, + label, + }); + + setLabels(uniq([...labels, label])); + ref.current.clear(); + }} + /> + } + maxWidth={'100%'} + /> + + {labels.map((label) => ( + + {label} + { + await removeLabelMutation.mutateAsync({ + id: organizerId, + scope, + label, + }); + + setLabels( + labels.filter((existingLabel) => label !== existingLabel), + ); + }} + /> + + ))} + + + {isInvalid && ( + {t('create.additionalInformation.labels.tips')} + )} + + ); +}; diff --git a/src/pages/organizers/[organizerId]/preview/OrganizerTable.tsx b/src/pages/organizers/[organizerId]/preview/OrganizerTable.tsx new file mode 100644 index 000000000..081e3bea9 --- /dev/null +++ b/src/pages/organizers/[organizerId]/preview/OrganizerTable.tsx @@ -0,0 +1,271 @@ +import { useTranslation } from 'react-i18next'; +import sanitizeHtml from 'sanitize-html'; + +import { SupportedLanguage } from '@/i18n/index'; +import { Organizer } from '@/types/Organizer'; +import { Image } from '@/ui/Image'; +import { Inline } from '@/ui/Inline'; +import { Link } from '@/ui/Link'; +import { Stack } from '@/ui/Stack'; +import { Text } from '@/ui/Text'; +import { colors, getValueFromTheme } from '@/ui/theme'; +import { + formatEmailAndPhone, + parseAddress, +} from '@/utils/formatOrganizerDetail'; +import { getLanguageObjectOrFallback } from '@/utils/getLanguageObjectOrFallback'; + +import { OrganizerLabelsForm } from './OrganizerLabels'; + +type Props = { organizer: Organizer }; + +const getGlobalValue = getValueFromTheme('global'); + +const { grey2, udbMainDarkGrey } = colors; + +const OrganizerInfo = ({ + title, + content, + urls, +}: { + title: string; + content: string; + urls?: string[]; +}) => { + const { t } = useTranslation(); + const isDescription = + title.startsWith('organizers.detail.description') || + title.startsWith('organizers.detail.educationalDescription'); + return ( + + + {t(title)} + + + {(urls ?? []).map((url) => ( + + {url} + + ))} + {isDescription ? ( + + ) : ( + + {content?.startsWith('organizers.detail.no') ? t(content) : content} + + )} + + + ); +}; + +const OrganizerImages = ({ + title, + organizer, + images, +}: { + title: string; + organizer: Organizer; + images: Organizer['images'] | undefined; +}) => { + const { t } = useTranslation(); + const isMainImage = (url: string) => { + return url === organizer?.mainImage; + }; + if (!images || images.length === 0) { + return ( + + + {t(title)} + + {t('organizers.detail.no_images')} + + ); + } + return ( + + + {t(title)} + + + {images?.map((image) => ( + + + + {image.description} + + + + {isMainImage(image.thumbnailUrl) && ( + + {t('organizers.detail.mainImage')} + + )} + {image.description} + + {`© ${image.copyrightHolder}`} + + + + ))} + + + ); +}; + +const OrganizerLabels = ({ + title, + organizer, +}: { + title: string; + organizer: Organizer; +}) => { + const { t } = useTranslation(); + return ( + + + {t(title)} + + + + ); +}; + +export const OrganizerTable = ({ organizer }: Props) => { + const { i18n } = useTranslation(); + + const formattedName: string = getLanguageObjectOrFallback( + organizer?.name, + i18n.language as SupportedLanguage, + organizer?.mainLanguage as SupportedLanguage, + ); + + const formattedDescription: string | undefined = organizer?.description + ? sanitizeHtml( + getLanguageObjectOrFallback( + organizer?.description, + i18n.language as SupportedLanguage, + organizer?.mainLanguage as SupportedLanguage, + ), + ) + : undefined; + + const formattedEducationalDescription: string | undefined = + organizer?.educationalDescription + ? sanitizeHtml( + getLanguageObjectOrFallback( + organizer?.educationalDescription, + i18n.language as SupportedLanguage, + organizer?.mainLanguage as SupportedLanguage, + ), + ) + : undefined; + + const formattedEmailAndPhone = + organizer?.contactPoint && + !Object.values(organizer.contactPoint).every((array) => array.length === 0) + ? formatEmailAndPhone(organizer?.contactPoint) + : 'organizers.detail.no_contact'; + + const formattedAddress = organizer?.address + ? parseAddress( + organizer, + i18n.language as SupportedLanguage, + organizer?.mainLanguage as SupportedLanguage, + ) + : 'organizers.detail.no_address'; + const organizerDetailInfo = [ + { title: 'organizers.detail.name', content: formattedName }, + formattedDescription && { + title: 'organizers.detail.description', + content: formattedDescription, + }, + formattedEducationalDescription && { + title: 'organizers.detail.educationalDescription', + content: formattedEducationalDescription, + }, + { title: 'organizers.detail.address', content: formattedAddress }, + { + title: 'organizers.detail.contact', + content: formattedEmailAndPhone, + urls: organizer?.contactPoint?.url, + }, + ].filter((item) => item); + return ( + + {organizerDetailInfo?.map((info) => ( + + ))} + + + + ); +}; diff --git a/src/pages/organizers/[organizerId]/preview/index.page.tsx b/src/pages/organizers/[organizerId]/preview/index.page.tsx new file mode 100644 index 000000000..7643e3204 --- /dev/null +++ b/src/pages/organizers/[organizerId]/preview/index.page.tsx @@ -0,0 +1,258 @@ +import getConfig from 'next/config'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { dehydrate, useQueryClient, UseQueryResult } from 'react-query'; + +import { + GetOrganizerPermissionsResponse, + useGetOrganizerByIdQuery, + useGetOrganizerPermissions, +} from '@/hooks/api/organizers'; +import { + OwnershipRequest, + RequestState, + useGetOwnershipRequestsQuery, + useRequestOwnershipMutation, +} from '@/hooks/api/ownerships'; +import { useGetUserQuery, User } from '@/hooks/api/user'; +import { SupportedLanguage } from '@/i18n/index'; +import { Organizer } from '@/types/Organizer'; +import { Alert, AlertVariants } from '@/ui/Alert'; +import { Box } from '@/ui/Box'; +import { Button, ButtonVariants } from '@/ui/Button'; +import { Icons } from '@/ui/Icon'; +import { Inline } from '@/ui/Inline'; +import { Link, LinkButtonVariants } from '@/ui/Link'; +import { Modal, ModalSizes, ModalVariants } from '@/ui/Modal'; +import { Page } from '@/ui/Page'; +import { Stack } from '@/ui/Stack'; +import { FetchError } from '@/utils/fetchFromApi'; +import { getApplicationServerSideProps } from '@/utils/getApplicationServerSideProps'; +import { getLanguageObjectOrFallback } from '@/utils/getLanguageObjectOrFallback'; + +import { OrganizerTable } from './OrganizerTable'; + +const OrganizersPreview = () => { + const { t, i18n } = useTranslation(); + const queryClient = useQueryClient(); + const [isQuestionModalVisible, setIsQuestionModalVisible] = useState(false); + const [isSuccessAlertVisible, setIsSuccessAlertVisible] = useState(false); + const [isErrorAlertVisible, setIsErrorAlertVisible] = useState(false); + const router = useRouter(); + const { publicRuntimeConfig } = getConfig(); + const isOwnershipEnabled = publicRuntimeConfig.ownershipEnabled === 'true'; + const organizerId = router.query.organizerId as string; + + const getOrganizerByIdQuery = useGetOrganizerByIdQuery({ + id: organizerId, + }) as UseQueryResult; + + const getOrganizerPermissionsQuery = useGetOrganizerPermissions({ + organizerId: organizerId, + }) as UseQueryResult; + + const permissions = getOrganizerPermissionsQuery?.data?.permissions ?? []; + const canEdit = permissions.includes('Organisaties bewerken'); + + const organizer: Organizer = getOrganizerByIdQuery?.data; + + const organizerName: string = getLanguageObjectOrFallback( + organizer?.name, + i18n.language as SupportedLanguage, + organizer?.mainLanguage as SupportedLanguage, + ); + + const getUserQuery = useGetUserQuery() as UseQueryResult; + + const userId = getUserQuery.data?.sub; + + const getOwnershipRequestsQuery = useGetOwnershipRequestsQuery({ + organizerId: organizerId, + ownerId: userId, + }); + + // @ts-expect-error + const userRequests = getOwnershipRequestsQuery?.data?.member; + const isOwnershipRequested = userRequests?.some( + (request: OwnershipRequest) => request.state === RequestState.REQUESTED, + ); + + const requestOwnershipMutation = useRequestOwnershipMutation({ + onSuccess: async () => { + await queryClient.invalidateQueries('ownership-requests'); + setIsSuccessAlertVisible(true); + setIsQuestionModalVisible(false); + }, + onError: () => { + setIsQuestionModalVisible(false); + setIsErrorAlertVisible(true); + }, + }); + + return ( + + {organizerName} + + + + {isOwnershipEnabled && ( + <> + setIsQuestionModalVisible(false)} + onConfirm={() => { + requestOwnershipMutation.mutate({ + itemId: organizerId, + ownerId: userId, + }); + }} + size={ModalSizes.MD} + > + + + + + {isOwnershipRequested && ( + + + + )} + {isSuccessAlertVisible && ( + { + setIsSuccessAlertVisible(false); + }} + > + + + )} + {isErrorAlertVisible && ( + { + setIsErrorAlertVisible(false); + }} + > + + + )} + + )} + + + + + + {!canEdit && isOwnershipEnabled && !isOwnershipRequested && ( + + )} + {canEdit && ( + + {t('organizers.detail.actions.edit')} + + )} + {canEdit && isOwnershipEnabled && !isOwnershipRequested && ( + + {t('organizers.detail.actions.manage')} + + )} + + {t('organizers.detail.actions.back')} + + + + + + + + ); +}; + +export const getServerSideProps = getApplicationServerSideProps( + async ({ req, query, cookies, queryClient }) => { + try { + await Promise.all([ + useGetOrganizerByIdQuery({ + req, + queryClient, + id: query.organizerId, + }), + useGetOrganizerPermissions({ + req, + queryClient, + organizerId: query.organizerId, + }), + ]); + } catch (error) { + console.error(error); + } + + return { + props: { + dehydratedState: dehydrate(queryClient), + cookies, + }, + }; + }, +); + +export default OrganizersPreview; diff --git a/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx b/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx index f527a9207..ada5124db 100644 --- a/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx +++ b/src/pages/steps/AdditionalInformationStep/LabelsStep.tsx @@ -22,12 +22,12 @@ import { Inline } from '@/ui/Inline'; import { getStackProps, Stack, StackProps } from '@/ui/Stack'; import { Text, TextVariants } from '@/ui/Text'; import { getGlobalBorderRadius, getValueFromTheme } from '@/ui/theme'; -import { Typeahead } from '@/ui/Typeahead'; +import { Typeahead, TypeaheadElement } from '@/ui/Typeahead'; import { getUniqueLabels } from '@/utils/getUniqueLabels'; type LabelsStepProps = StackProps & TabContentProps; -const LABEL_PATTERN = /^[0-9a-zA-ZÀ-ÿ][0-9a-zA-ZÀ-ÿ\-_\s]{1,49}$/; +export const LABEL_PATTERN = /^[0-9a-zA-ZÀ-ÿ][0-9a-zA-ZÀ-ÿ\-_\s]{1,49}$/; const getGlobalValue = getValueFromTheme('global'); const getButtonValue = getValueFromTheme('button'); @@ -44,7 +44,7 @@ function LabelsStep({ // @ts-expect-error const entity: Offer | Organizer | undefined = getEntityByIdQuery.data; - const ref = useRef(null); + const ref = useRef>(null); const [query, setQuery] = useState(''); // @ts-expect-error @@ -65,6 +65,7 @@ function LabelsStep({ const isWriting = addLabelMutation.isLoading || removeLabelMutation.isLoading; const [isInvalid, setIsInvalid] = useState(false); + return ( {label} { await removeLabelMutation.mutateAsync({ diff --git a/src/redirects.tsx b/src/redirects.tsx index bc4d75bac..775409a86 100644 --- a/src/redirects.tsx +++ b/src/redirects.tsx @@ -96,6 +96,11 @@ const getRedirects = ( destination: '/organizers/:organizerId/edit', permanent: false, }, + { + source: '/organizer/:organizerId/preview', + destination: '/organizers/:organizerId/preview', + permanent: false, + }, { source: '/organizer/:organizerId/ownerships', destination: '/organizers/:organizerId/ownerships', diff --git a/src/types/Organizer.ts b/src/types/Organizer.ts index 27ca5b64b..3eb8ff493 100644 --- a/src/types/Organizer.ts +++ b/src/types/Organizer.ts @@ -20,12 +20,14 @@ type Organizer = { completeness: number; modified: string; images: MediaObject[]; + mainImage: string; geo: { latitude: number; longitude: number; }; location?: Address; url?: string; + description?: string; educationalDescription?: string; }; diff --git a/src/ui/Inline.tsx b/src/ui/Inline.tsx index 4b9277953..4f1979a82 100644 --- a/src/ui/Inline.tsx +++ b/src/ui/Inline.tsx @@ -95,6 +95,8 @@ const inlinePropTypes = [ 'stackOn', ]; +const linkPropTypes = ['rel', 'target']; + const getInlineProps = (props: UnknownProps) => pickBy(props, (_value, key) => { // pass aria attributes to the DOM element @@ -102,7 +104,11 @@ const getInlineProps = (props: UnknownProps) => return true; } - const propTypes: string[] = [...boxPropTypes, ...inlinePropTypes]; + const propTypes: string[] = [ + ...boxPropTypes, + ...inlinePropTypes, + ...linkPropTypes, + ]; return propTypes.includes(key); }); diff --git a/src/ui/Link.tsx b/src/ui/Link.tsx index 5f909a138..c5278074c 100644 --- a/src/ui/Link.tsx +++ b/src/ui/Link.tsx @@ -14,7 +14,7 @@ import { getValueFromTheme } from './theme'; const getValue = getValueFromTheme('link'); -const LinkButtonVariants = { +export const LinkButtonVariants = { BUTTON_PRIMARY: 'primary', BUTTON_SECONDARY: 'secondary', BUTTON_DANGER: 'danger', @@ -61,7 +61,7 @@ const BaseLink = forwardRef( alignItems="center" {...getInlineProps(props)} > - @@ -135,14 +135,16 @@ const Link = ({ : undefined; const inner = [ - iconName && , - customChildren - ? children - : !shouldHideText && ( - - {children} - - ), + + {iconName && } + {customChildren + ? children + : !shouldHideText && ( + + {children} + + )} + , clonedSuffix, ]; diff --git a/src/ui/Typeahead.tsx b/src/ui/Typeahead.tsx index 0b8d97bc7..aa58692f3 100644 --- a/src/ui/Typeahead.tsx +++ b/src/ui/Typeahead.tsx @@ -1,6 +1,6 @@ import 'react-bootstrap-typeahead/css/Typeahead.css'; -import type { ForwardedRef } from 'react'; +import type { ForwardedRef, Ref } from 'react'; import { forwardRef } from 'react'; import type { TypeaheadModel } from 'react-bootstrap-typeahead'; import { AsyncTypeahead as BootstrapTypeahead } from 'react-bootstrap-typeahead'; @@ -24,6 +24,9 @@ type NewEntry = { label: string; }; +export type TypeaheadElement = + BootstrapTypeahead; + const isNewEntry = (value: any): value is NewEntry => { return !!value?.customOption; }; @@ -65,7 +68,7 @@ const TypeaheadInner = ( isLoading, ...props }: Props, - ref: ForwardedRef, + ref: ForwardedRef>, ) => { const { t } = useTranslation(); @@ -83,7 +86,7 @@ const TypeaheadInner = ( disabled={disabled} className={className} flex={1} - ref={ref} + ref={ref as unknown as Ref} css={` input[type='time']::-webkit-calendar-picker-indicator { display: none; @@ -164,7 +167,7 @@ const TypeaheadInner = ( }; const Typeahead = forwardRef(TypeaheadInner) as ( - props: Props & { ref?: ForwardedRef }, + props: Props & { ref?: ForwardedRef> }, ) => ReturnType>; export type { NewEntry }; diff --git a/src/utils/formatOrganizerDetail.ts b/src/utils/formatOrganizerDetail.ts new file mode 100644 index 000000000..19dc8b35e --- /dev/null +++ b/src/utils/formatOrganizerDetail.ts @@ -0,0 +1,28 @@ +import { getAddress } from '@/pages/create/OfferForm'; +import { Organizer } from '@/types/Organizer'; + +import { SupportedLanguage } from '../i18n'; + +const parseAddress = ( + offer: Organizer, + language: SupportedLanguage, + mainLanguage: SupportedLanguage, +) => { + const { addressLocality, postalCode, streetAddress } = getAddress( + offer.address, + language, + mainLanguage, + ); + + return `${streetAddress}\n${postalCode} ${addressLocality}`; +}; + +const formatEmailAndPhone = ({ email, phone }: Organizer['contactPoint']) => { + const contactDetails = [...email, ...phone]; + if (contactDetails.length === 0) { + return null; + } + return contactDetails.join('\n'); +}; + +export { formatEmailAndPhone, parseAddress }; diff --git a/src/utils/handleErrorObject.ts b/src/utils/handleErrorObject.ts new file mode 100644 index 000000000..2dd5acb16 --- /dev/null +++ b/src/utils/handleErrorObject.ts @@ -0,0 +1,5 @@ +import { ErrorObject, isErrorObject } from './fetchFromApi'; + +export const handleErrorObject = async (response: ErrorObject | Response) => + // eslint-disable-next-line no-console + isErrorObject(response) ? console.error(response) : await response.json(); diff --git a/yarn.lock b/yarn.lock index fbcfce1f3..71d4acc27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6445,6 +6445,15 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -6455,7 +6464,7 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== -domelementtype@^2.0.1, domelementtype@^2.2.0: +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== @@ -6474,6 +6483,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" @@ -6483,6 +6499,15 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -6656,6 +6681,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -8327,6 +8357,16 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" +htmlparser2@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -8869,7 +8909,7 @@ is-plain-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== -is-plain-object@5.0.0: +is-plain-object@5.0.0, is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== @@ -10467,6 +10507,11 @@ nanoid@^3.3.1, nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -11087,6 +11132,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== + parse5@6.0.1, parse5@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -11274,6 +11324,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -11491,6 +11546,15 @@ postcss@^8.2.15: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.3.11: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postinstall-postinstall@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" @@ -12602,6 +12666,18 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" +sanitize-html@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.1.tgz#b4639b0a09574ab62b1b353cb99b1b87af742834" + integrity sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^8.0.0" + is-plain-object "^5.0.0" + parse-srcset "^1.0.2" + postcss "^8.3.11" + sass-loader@^10.0.4: version "10.4.1" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.4.1.tgz#bea4e173ddf512c9d7f53e9ec686186146807cbf" @@ -12962,6 +13038,11 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"