diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index abcc42d91..3b3506a82 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,7 @@ # CODEOWNERS are automatically assigned as possible reviewers to new PRs. # Global owners (also need to be duplicated in later rules) -* @simon-debruijn @brampauwelyn @Anahkiasen +* @simon-debruijn @brampauwelyn @Anahkiasen @vhande # Jenkins / deployment owners Gemfile* @willaerk @paulherbosch diff --git a/package.json b/package.json index 6c6f3dcb8..c134343a0 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.2", - "@playwright/test": "^1.31.2", + "@playwright/test": "1.31.2", "@storybook/addon-a11y": "^6.5.16", "@storybook/addon-actions": "^6.5.16", "@storybook/addon-essentials": "^6.5.16", diff --git a/src/hooks/useHandleWindowMessage.js b/src/hooks/useHandleWindowMessage.js index 362c00f88..069222f73 100644 --- a/src/hooks/useHandleWindowMessage.js +++ b/src/hooks/useHandleWindowMessage.js @@ -13,6 +13,7 @@ const WindowMessageTypes = { HTTP_ERROR_CODE: 'HTTP_ERROR_CODE', OFFER_MODERATED: 'OFFER_MODERATED', OPEN_ANNOUNCEMENT_MODAL: 'OPEN_ANNOUNCEMENT_MODAL', + PAGE_HEIGHT: 'PAGE_HEIGHT', }; const useHandleWindowMessage = (eventsMap = {}) => { diff --git a/src/hooks/useLegacyPath.tsx b/src/hooks/useLegacyPath.tsx new file mode 100644 index 000000000..cbfd4790f --- /dev/null +++ b/src/hooks/useLegacyPath.tsx @@ -0,0 +1,41 @@ +import getConfig from 'next/config'; +import { useRouter } from 'next/router'; +import { useMemo } from 'react'; + +import { useCookiesWithOptions } from './useCookiesWithOptions'; + +const useLegacyPath = () => { + const { cookies } = useCookiesWithOptions(['token', 'udb-language']); + const router = useRouter(); + const { publicRuntimeConfig } = getConfig(); + const prefixWhenNotEmpty = (value, prefix) => + value ? `${prefix}${value}` : value; + + const jwt = cookies.token; + 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, + }), + '?', + ); + + return `${publicRuntimeConfig.legacyAppUrl}${path}${queryString}`; + }, [ + router.asPath, + router.query, + jwt, + lang, + publicRuntimeConfig.legacyAppUrl, + ]); + + return legacyPath; +}; + +export { useLegacyPath }; diff --git a/src/i18n/de.json b/src/i18n/de.json index fcc710ec5..d2bae64b2 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -1056,6 +1056,11 @@ } } }, + "search": { + "title": "Suchen", + "events_places": "Veranstaltungen und Orte", + "organizers": "Organisationen" + }, "selectionTable": { "rowsSelectedCount": "{{count}} Reihe ausgewählt", "rowsSelectedCount_plural": "{{count}} Reihen ausgewählt" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 65b108453..5de57e84f 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -1056,6 +1056,11 @@ } } }, + "search": { + "title": "Chercher", + "events_places": "Événements et Lieux", + "organizers": "Organisations" + }, "selectionTable": { "rowsSelectedCount": "{{count}} ligne sélectionnée", "rowsSelectedCount_plural": "{{count}} lignes sélectionnées" diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 302dbb385..b31828c9f 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -1065,6 +1065,11 @@ } } }, + "search": { + "title": "Zoeken", + "events_places": "Evenementen en locaties", + "organizers": "Organisaties" + }, "selectionTable": { "rowsSelectedCount": "{{count}} rij geselecteerd", "rowsSelectedCount_plural": "{{count}} rijen geselecteerd" diff --git a/src/layouts/index.js b/src/layouts/index.js index ffdbeb949..37052a6f8 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -67,20 +67,27 @@ const Layout = ({ children }) => { useChangeLanguage(); useHandleWindowMessage({ [WindowMessageTypes.URL_CHANGED]: ({ path }) => { - const currentPath = window.location.pathname; - const url = new URL( + const currentUrl = new URL(window.location.href); + const nextUrl = new URL( `${window.location.protocol}//${window.location.host}${path}`, ); - const query = Object.fromEntries(url.searchParams.entries()); - const hasPage = url.searchParams.has('page'); + + const areUrlsDeepEqual = + currentUrl.pathname === nextUrl.pathname && + [...nextUrl.searchParams.entries()] + .filter(([key]) => key !== 'jwt' && key !== 'lang') + .every(([key, val]) => currentUrl.searchParams.get(key) === val); + + if (areUrlsDeepEqual) { + return; + } + + const hasPage = nextUrl.searchParams.has('page'); + if (hasPage) { - window.history.pushState( - undefined, - '', - `${window.location.protocol}//${window.location.host}${path}`, - ); + window.history.pushState(undefined, '', nextUrl.toString()); } else { - router.push({ pathname: url.pathname, query }); + router.push(`${nextUrl.pathname}${nextUrl.search}`); } }, [WindowMessageTypes.HTTP_ERROR_CODE]: ({ code }) => { diff --git a/src/middleware.api.ts b/src/middleware.api.ts index 360555794..994a67820 100644 --- a/src/middleware.api.ts +++ b/src/middleware.api.ts @@ -45,14 +45,50 @@ export const middleware = async (request: NextRequest) => { return NextResponse.redirect(url); } - const isOwnershipPage = request.nextUrl.pathname.endsWith('ownerships'); + const isOwnershipPage = + request.nextUrl.pathname.startsWith('/organizer') && + !request.nextUrl.pathname.endsWith('/ownerships'); - if (isOwnershipPage && !process.env.NEXT_PUBLIC_OWNERSHIP_ENABLED) { - const url = new URL('/404', request.url); - return NextResponse.redirect(url); + 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'], + matcher: [ + '/event', + '/login', + '/organizers/:id/ownerships', + '/organizer/(.*)', + '/(.*)/ownerships', + '/search(.*)', + '/[...params]', + ], }; diff --git a/src/pages/[...params].page.js b/src/pages/[...params].page.js index b61fea2b6..144b96841 100644 --- a/src/pages/[...params].page.js +++ b/src/pages/[...params].page.js @@ -35,13 +35,6 @@ IFrame.propTypes = { const Fallback = () => { const router = useRouter(); - - const { - // eslint-disable-next-line no-unused-vars - query: { params = [], ...queryWithoutParams }, - asPath, - } = router; - const { publicRuntimeConfig } = getConfig(); // Keep track of which paths were not found. Do not store as a single boolean @@ -51,37 +44,37 @@ const Fallback = () => { const [notFoundPaths, setNotFoundPaths] = useState(['/404']); useHandleWindowMessage({ [WindowMessageTypes.URL_UNKNOWN]: () => - setNotFoundPaths([asPath, ...notFoundPaths]), + setNotFoundPaths([router.asPath, ...notFoundPaths]), }); 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${asPath}`).pathname; - const ownershipPaths = - (router.asPath.startsWith('/organizer') && - !router.asPath.endsWith('/ownerships')) || - router.asPath.startsWith('/search'); + const path = new URL(`http://localhost${router.asPath}`).pathname; + const { params = [], ...queryWithoutParams } = router.query; const queryString = prefixWhenNotEmpty( new URLSearchParams({ ...queryWithoutParams, - jwt: cookies.token, - lang: cookies['udb-language'], - ...(ownershipPaths && - publicRuntimeConfig.ownershipEnabled === 'true' && { - ownership: 'true', - }), + jwt: token, + lang: language, }), '?', ); return `${publicRuntimeConfig.legacyAppUrl}${path}${queryString}`; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [asPath, cookies.token, cookies['udb-language']]); - - if (notFoundPaths.includes(asPath)) { + }, [ + language, + publicRuntimeConfig.legacyAppUrl, + router.asPath, + router.query, + token, + ]); + + if (notFoundPaths.includes(router.asPath)) { return ; } diff --git a/src/pages/organizers/[organizerId]/ownerships/index.page.tsx b/src/pages/organizers/[organizerId]/ownerships/index.page.tsx index 959ab0f72..e13061e09 100644 --- a/src/pages/organizers/[organizerId]/ownerships/index.page.tsx +++ b/src/pages/organizers/[organizerId]/ownerships/index.page.tsx @@ -66,6 +66,9 @@ const Ownership = () => { // @ts-expect-error const organizer: Organizer = getOrganizerByIdQuery?.data; + const organizerName = + organizer?.name?.[i18n.language] ?? + organizer?.name?.[organizer.mainLanguage]; const getOwnershipRequestsQuery = useGetOwnershipRequestsQuery({ organizerId, @@ -139,7 +142,7 @@ const Ownership = () => { {t('organizers.ownerships.title', { - name: organizer?.name?.[i18n.language], + name: organizerName, })} @@ -163,7 +166,7 @@ const Ownership = () => { i18nKey={`${translationsPath}.success`} values={{ ownerEmail: selectedRequest?.ownerEmail, - organizerName: organizer?.name?.[i18n.language], + organizerName: organizerName, }} /> @@ -253,7 +256,7 @@ const Ownership = () => { i18nKey={`${translationsPath}.body`} values={{ ownerEmail: selectedRequest?.ownerEmail, - organizerName: organizer?.name?.[i18n.language], + organizerName: organizerName, }} /> diff --git a/src/pages/search/index.page.tsx b/src/pages/search/index.page.tsx new file mode 100644 index 000000000..c052250d6 --- /dev/null +++ b/src/pages/search/index.page.tsx @@ -0,0 +1,128 @@ +import getConfig from 'next/config'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { TabContent } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { + useHandleWindowMessage, + WindowMessageTypes, +} from '@/hooks/useHandleWindowMessage'; +import { useIsClient } from '@/hooks/useIsClient'; +import { useLegacyPath } from '@/hooks/useLegacyPath'; +import { Page } from '@/ui/Page'; +import { Stack } from '@/ui/Stack'; +import { Tabs, TabsVariants } from '@/ui/Tabs'; +import { colors } from '@/ui/theme'; +import { getApplicationServerSideProps } from '@/utils/getApplicationServerSideProps'; + +import { Scope } from '../create/OfferForm'; + +const Search = () => { + const { t } = useTranslation(); + const [iframeHeight, setIframeHeight] = useState(0); + const legacyPath = useLegacyPath(); + const { query, ...router } = useRouter(); + const tab = (query?.tab as Scope) ?? 'events-places'; + const { udbMainDarkBlue } = colors; + const isClientSide = useIsClient(); + const { publicRuntimeConfig } = getConfig(); + const isOwnershipEnabled = publicRuntimeConfig.ownershipEnabled === 'true'; + + const handleSelectTab = async (tabKey: Scope) => + router.push( + { + pathname: '/search', + query: { + tab: tabKey, + }, + }, + undefined, + { shallow: true }, + ); + + useHandleWindowMessage({ + [WindowMessageTypes.PAGE_HEIGHT]: (event) => setIframeHeight(event?.height), + }); + + return ( + + {t('search.title')} + + + {isOwnershipEnabled && ( + + activeKey={tab} + onSelect={handleSelectTab} + activeBackgroundColor={`${udbMainDarkBlue}`} + variant={TabsVariants.OUTLINED} + css={` + .nav-tabs { + margin-left: 1.5rem; + } + `} + > + + {tab === 'events-places' && ( + + + {isClientSide && ( + + )} + + + )} + + + {tab === 'organizers' && ( + + + {isClientSide && ( + + )} + + + )} + + + )} + {!isOwnershipEnabled && isClientSide && ( + + )} + + + + ); +}; + +export const getServerSideProps = getApplicationServerSideProps(); + +export default Search; diff --git a/src/pages/steps/LocationStep.tsx b/src/pages/steps/LocationStep.tsx index 6a83968df..0ca07a1f6 100644 --- a/src/pages/steps/LocationStep.tsx +++ b/src/pages/steps/LocationStep.tsx @@ -268,7 +268,6 @@ export const BlankStreetToggle = ({ return ( @@ -708,14 +707,16 @@ const LocationStep = ({ t('location.add_modal.errors.streetAndNumber') } info={ - - onFieldChange({ - streetAndNumber, - location: { streetAndNumber }, - }) - } - /> + scope === ScopeTypes.ORGANIZERS && ( + + onFieldChange({ + streetAndNumber, + location: { streetAndNumber }, + }) + } + /> + ) } /> diff --git a/src/redirects.tsx b/src/redirects.tsx index 195506fed..bc4d75bac 100644 --- a/src/redirects.tsx +++ b/src/redirects.tsx @@ -1,3 +1,5 @@ +import getConfig from 'next/config'; + import { FeatureFlags } from './hooks/useFeatureFlag'; import type { SupportedLanguages } from './i18n'; import type { Values } from './types/Values'; @@ -39,6 +41,8 @@ const createDashboardRedirects = (environment: Environment) => { ]; }; +const { publicRuntimeConfig } = getConfig(); + const getRedirects = ( environment: Environment, language: Values = 'nl', @@ -97,6 +101,16 @@ const getRedirects = ( destination: '/organizers/:organizerId/ownerships', permanent: false, }, + publicRuntimeConfig.ownershipEnabled === 'true' && { + source: '/manage/organizations', + destination: '/search?tab=organizers', + permanent: false, + }, + publicRuntimeConfig.ownershipEnabled === 'false' && { + source: '/search?tab=organizers', + destination: '/404', + permanent: false, + }, { source: '/:language/copyright', destination: '/copyright', diff --git a/src/test/e2e/create-place.spec.ts b/src/test/e2e/create-place.spec.ts index d255f532a..6c99e2671 100644 --- a/src/test/e2e/create-place.spec.ts +++ b/src/test/e2e/create-place.spec.ts @@ -51,6 +51,7 @@ test('create a place', async ({ baseURL, page }) => { await page .getByRole('option', { name: dummyPlace.address.municipality }) .click(); + await expect(page.getByLabel('blank_address')).not.toBeVisible(); await page.getByLabel('Straat en nummer').nth(0).click(); await page .getByLabel('Straat en nummer') diff --git a/src/ui/Tabs.tsx b/src/ui/Tabs.tsx index 420272361..6078c0d05 100644 --- a/src/ui/Tabs.tsx +++ b/src/ui/Tabs.tsx @@ -6,19 +6,30 @@ import { TabsProps, } from 'react-bootstrap'; +import { Values } from '@/types/Values'; import type { BoxProps } from '@/ui/Box'; import { Box, getBoxProps, parseSpacing } from '@/ui/Box'; -import { getValueFromTheme } from './theme'; +import { colors, getValueFromTheme } from './theme'; const getValue = getValueFromTheme(`tabs`); -type Props = BoxProps & TabsProps & { activeBackgroundColor?: string }; +export const TabsVariants = { + DEFAULT: 'default', + OUTLINED: 'outlined', +} as const; + +type Props = BoxProps & + Omit & { + activeBackgroundColor?: string; + variant?: Values; + }; const Tabs = ({ activeKey, onSelect, - activeBackgroundColor, + activeBackgroundColor = 'white', + variant = TabsVariants.DEFAULT, children: rawChildren, className, ...props @@ -39,45 +50,88 @@ const Tabs = ({ return true; }); + const { udbMainDarkBlue, grey1 } = colors; + const TabStyles = { + default: ` + border-bottom: none; + + .nav-item:last-child { + border-right: 1px solid ${getValue('borderColor')}; + } + + .nav-item { + background-color: white; + color: ${getValue('color')}; + border-radius: ${getValue('borderRadius')}; + padding: ${parseSpacing(3)} ${parseSpacing(4)}; + border-color: ${getValue('borderColor')}; + border-right: none; + + &.nav-link { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + &.active { + background-color: ${activeBackgroundColor}; + border-bottom-color: ${getValue('activeTabBackgroundColor')}; + cursor: default; + border-bottom: transparent; + } + + &:hover { + color: ${getValue('hoverColor')}; + border-color: transparent; + background-color: ${getValue('hoverTabBackgroundColor')}; + } + } + `, + outlined: ` + border-bottom: none; + .nav { + margin-left: 1.5rem; + margin-bottom: 1.5rem; + } + .nav-item.nav-link { + padding: 0.4rem 1rem; + border: 1px solid black; + } + .nav-item { + margin: 0 !important; + border-radius: 0; + background-color: white; + + &:hover { + background-color: ${grey1}; + } + + &:first-child { + border-right: none; + border-radius: 0.5rem 0 0 0.5rem; + } + + &:last-child { + border-radius: 0 0.5rem 0.5rem 0; + } + + &.active { + color: white; + background-color: ${udbMainDarkBlue}; + } + + &.active:hover { + background-color: ${udbMainDarkBlue}; + } + } +`, + }; + return ( {children} diff --git a/yarn.lock b/yarn.lock index 374157541..fbcfce1f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1767,7 +1767,7 @@ tiny-glob "^0.2.9" tslib "^2.4.0" -"@playwright/test@^1.31.2": +"@playwright/test@1.31.2": version "1.31.2" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.31.2.tgz#426d8545143a97a6fed250a2a27aa1c8e5e2548e" integrity sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==