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==