From 9cac366bd652df7850445afc253618de79f2fa41 Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 25 Oct 2023 11:43:17 +0200 Subject: [PATCH] header refactor --- pwa/package-lock.json | 8 +- pwa/package.json | 2 +- pwa/src/apiService/apiService.ts | 16 ++ pwa/src/apiService/resources/headerContent.ts | 16 ++ pwa/src/hooks/headerContent.ts | 25 ++ .../templateParts/header/HeaderContent.json | 174 +++++++++++++ .../header/HeaderTemplate.module.css | 4 + .../templateParts/header/HeaderTemplate.tsx | 236 ++++++++++-------- pwa/static/.env.development | 2 + pwa/static/.env.production | 9 + 10 files changed, 383 insertions(+), 109 deletions(-) create mode 100644 pwa/src/apiService/resources/headerContent.ts create mode 100644 pwa/src/hooks/headerContent.ts create mode 100644 pwa/src/templates/templateParts/header/HeaderContent.json diff --git a/pwa/package-lock.json b/pwa/package-lock.json index dd404a6d5..5b5af1b17 100644 --- a/pwa/package-lock.json +++ b/pwa/package-lock.json @@ -8,7 +8,7 @@ "name": "skeleton-pip", "version": "1.0.0", "dependencies": { - "@conduction/components": "2.2.19", + "@conduction/components": "2.2.20", "@conduction/theme": "1.0.50", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-brands-svg-icons": "6.4.2", @@ -1897,9 +1897,9 @@ } }, "node_modules/@conduction/components": { - "version": "2.2.19", - "resolved": "https://registry.npmjs.org/@conduction/components/-/components-2.2.19.tgz", - "integrity": "sha512-sT35LBUs0oMvR6OSsugWsF+TZXvUJtyC+PKyb8ag+dTP2dVkGBgV8FX4SYTnXU36wD1sxjNlXBuRV47FIkEnVA==", + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/@conduction/components/-/components-2.2.20.tgz", + "integrity": "sha512-FsvkfRebglGNmx3bQE+U+02K85Q6GDs2hm8tMwb5N+lWfQemZ2iqf45J/io7mrQS0d3ctkqMwVBjTpWSJEJ4WQ==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.2.0", diff --git a/pwa/package.json b/pwa/package.json index a2dcde0a2..c7aa935f8 100644 --- a/pwa/package.json +++ b/pwa/package.json @@ -23,7 +23,7 @@ "prepare": "cd .. && husky install" }, "dependencies": { - "@conduction/components": "2.2.19", + "@conduction/components": "2.2.20", "@conduction/theme": "1.0.50", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-brands-svg-icons": "6.4.2", diff --git a/pwa/src/apiService/apiService.ts b/pwa/src/apiService/apiService.ts index 22338e681..854e1a47b 100644 --- a/pwa/src/apiService/apiService.ts +++ b/pwa/src/apiService/apiService.ts @@ -14,6 +14,8 @@ import FooterContent from "./resources/footerContent"; import Login from "./services/login"; import Me from "./services/me"; +import { DEFAULT_HEADER_CONTENT_URL } from "../templates/templateParts/header/HeaderTemplate"; +import HeaderContent from "./resources/headerContent"; export default class APIService { public JWT?: string; @@ -85,6 +87,16 @@ export default class APIService { }); } + public get HeaderContentClient(): AxiosInstance { + return axios.create({ + baseURL: removeFileNameFromUrl( + process.env.GATSBY_HEADER_CONTENT !== undefined && process.env.GATSBY_HEADER_CONTENT.length !== 0 + ? process.env.GATSBY_HEADER_CONTENT + : DEFAULT_HEADER_CONTENT_URL, + ), + }); + } + // Resources public get Case(): Case { return new Case(this.apiClient); @@ -122,6 +134,10 @@ export default class APIService { return new FooterContent(this.FooterContentClient); } + public get HeaderContent(): HeaderContent { + return new HeaderContent(this.HeaderContentClient); + } + // Services public get Login(): Login { return new Login(this.LoginClient); diff --git a/pwa/src/apiService/resources/headerContent.ts b/pwa/src/apiService/resources/headerContent.ts new file mode 100644 index 000000000..effd5c61e --- /dev/null +++ b/pwa/src/apiService/resources/headerContent.ts @@ -0,0 +1,16 @@ +import { Send } from "../apiService"; +import { AxiosInstance } from "axios"; + +export default class HeaderContent { + private _instance: AxiosInstance; + + constructor(_instance: AxiosInstance) { + this._instance = _instance; + } + + public getContent = async (fileName: string): Promise => { + const { data } = await Send(this._instance, "GET", fileName); + + return data; + }; +} diff --git a/pwa/src/hooks/headerContent.ts b/pwa/src/hooks/headerContent.ts new file mode 100644 index 000000000..15ee8bc9d --- /dev/null +++ b/pwa/src/hooks/headerContent.ts @@ -0,0 +1,25 @@ +import * as React from "react"; +import { useQuery } from "react-query"; +import APIService from "../apiService/apiService"; +import APIContext from "../apiService/apiContext"; +import { getFileNameFromUrl } from "../services/FileNameFromUrl"; +import { DEFAULT_HEADER_CONTENT_URL } from "../templates/templateParts/header/HeaderTemplate"; + +export const useHeaderContent = () => { + const API: APIService | null = React.useContext(APIContext); + + const fileName = getFileNameFromUrl( + process.env.GATSBY_HEADER_CONTENT !== undefined && process.env.GATSBY_HEADER_CONTENT.length !== 0 + ? process.env.GATSBY_HEADER_CONTENT + : DEFAULT_HEADER_CONTENT_URL, + ); + + const getContent = () => + useQuery(["contents", fileName], () => API?.HeaderContent.getContent(fileName), { + onError: (error) => { + console.warn(error.message); + }, + }); + + return { getContent }; +}; diff --git a/pwa/src/templates/templateParts/header/HeaderContent.json b/pwa/src/templates/templateParts/header/HeaderContent.json new file mode 100644 index 000000000..458db3cb6 --- /dev/null +++ b/pwa/src/templates/templateParts/header/HeaderContent.json @@ -0,0 +1,174 @@ +[ + { + "label": "Home", + "type": "internal", + + "current": { + "pathname": "/", + "operator": "equals" + }, + "handleClick": { + "link": "/" + } + }, + { + "label": "Categories", + "type": "internal", + "current": { + "pathname": "/categories", + "operator": "includes" + }, + "handleClick": { + "link": "/categories" + } + }, + { + "label": "Applications", + "type": "internal", + "current": { + "pathname": "/applications", + "operator": "includes" + }, + "handleClick": { + "link": "/applications" + } + }, + { + "label": "Components", + "current": { + "pathname": "/components", + "operator": "includes" + }, + "subItems": [ + { + "label": "All components", + "type": "internal", + "current": { + "pathname": "/components", + "operator": "includes" + }, + "handleClick": { + "link": "/components" + } + }, + { + "label": "Processes", + "type": "internal", + "current": { + "pathname": "/components", + "operator": "includes", + "filterCondition": { + "filter": "embedded.nl.embedded.commonground.layerType", + "value": "process", + "isObject": true + } + }, + "handleClick": { + "link": "/components", + "type": "internal", + "setFilter": { + "filter": "embedded.nl.embedded.commonground.layerType", + "value": "process", + "isObject": true + } + } + }, + { + "label": "Data models", + "type": "internal", + "current": { + "pathname": "/components", + "operator": "includes", + "filterCondition": { + "filter": "embedded.nl.embedded.commonground.layerType", + "value": "data", + "isObject": true + } + }, + "handleClick": { + "link": "/components", + "setFilter": { + "filter": "embedded.nl.embedded.commonground.layerType", + "value": "data", + "isObject": true + } + } + }, + { + "label": "API's", + "type": "internal", + "current": { + "pathname": "/components", + "operator": "includes", + "filterCondition": { + "filter": "embedded.nl.embedded.commonground.layerType", + "value": "service", + "isObject": true + } + }, + "handleClick": { + "link": "/components", + "setFilter": { + "filter": "embedded.nl.embedded.commonground.layerType", + "value": "service", + "isObject": true + } + } + } + ] + }, + { + "label": "Organizations", + "type": "internal", + "current": { + "pathname": "/organizations", + "operator": "includes" + }, + "handleClick": { + "link": "/organizations" + } + }, + { + "label": "Initiatives", + "type": "internal", + "current": "pathname === \"/components\" && filters.developmentStatus === \"concept\"", + "handleClick": { + "link": "/components", + "setFilter": { + "filter": "developmentStatus", + "value": "concept" + } + } + }, + { + "label": "Documentatie", + "current": { + "pathname": "/documentation", + "operator": "includes" + }, + "subItems": [ + { + "label": "About", + "type": "internal", + "current": { + "pathname": "/documentation/about", + "operator": "equals" + }, + "handleClick": { + "link": "/documentation/about" + } + }, + { + "label": "Use", + "type": "internal", + "current": { + "pathname": "/documentation/usage", + "operator": "equals" + }, + "handleClick": { + "link": "/documentation/usage" + } + } + ] + } +] diff --git a/pwa/src/templates/templateParts/header/HeaderTemplate.module.css b/pwa/src/templates/templateParts/header/HeaderTemplate.module.css index 5133fd481..15781ae65 100644 --- a/pwa/src/templates/templateParts/header/HeaderTemplate.module.css +++ b/pwa/src/templates/templateParts/header/HeaderTemplate.module.css @@ -158,6 +158,10 @@ li .denhaag-link__icon { padding-inline: var(--web-app-size-md); } + .headerContainerSingle { + padding-block-start: var(--web-app-size-md); + } + .headerContent { padding-block-start: var(--web-app-size-3xl); padding-block-end: var(--web-app-size-3xl); diff --git a/pwa/src/templates/templateParts/header/HeaderTemplate.tsx b/pwa/src/templates/templateParts/header/HeaderTemplate.tsx index 98681c6e5..22c34662a 100644 --- a/pwa/src/templates/templateParts/header/HeaderTemplate.tsx +++ b/pwa/src/templates/templateParts/header/HeaderTemplate.tsx @@ -15,6 +15,10 @@ import { PageHeader } from "@utrecht/component-library-react"; import { isHomepage } from "../../../services/isHomepage"; import { Breadcrumbs } from "../../../components/breadcrumbs/Breadcrumbs"; import { ITopNavItem } from "@conduction/components/lib/components/topNav/primaryTopNav/PrimaryTopNav"; +import { useHeaderContent } from "../../../hooks/headerContent"; + +export const DEFAULT_HEADER_CONTENT_URL = + "https://raw.githubusercontent.com/OpenCatalogi/web-app/348679b7537b20e51767dfdc6086349602afe219/pwa/src/templates/templateParts/header/HeaderContent.json"; interface HeaderTemplateProps { layoutClassName?: string; @@ -23,7 +27,7 @@ interface HeaderTemplateProps { export const HeaderTemplate: React.FC = ({ layoutClassName }) => { const { t } = useTranslation(); const [filters, setFilters] = React.useContext(FiltersContext); - const [topNavItems, setTopNavItems] = React.useState([]); + const [topNavItems, setTopNavItems] = React.useState([]); const { pageContext: { @@ -33,109 +37,22 @@ export const HeaderTemplate: React.FC = ({ layoutClassName screenSize, } = React.useContext(GatsbyContext); - const primaryTopNavItems: ITopNavItem[] = [ - { - label: "Home", - current: - pathname === "/" || - (process.env.GATSBY_USE_GITHUB_REPOSITORY_NAME_AS_PATH_PREFIX === "true" && - pathname === `/${process.env.GATSBY_GITHUB_REPOSITORY_NAME}/`), - handleClick: () => { - navigate("/"); - }, - }, - { - label: t("Categories"), - current: pathname.includes("/categories"), - handleClick: () => { - navigate("/categories"); - }, - }, - { - label: t("Applications"), - current: pathname.includes("/applications"), - handleClick: () => { - navigate("/applications"); - }, - }, - { - label: t("Components"), - current: pathname.includes("/components"), - subItems: [ - { - label: t("All components"), - current: pathname.includes("/components"), - handleClick: () => { - navigate("/components"); - }, - }, - { - label: t("Processes"), - current: - pathname === "/components" && filters["embedded.nl.embedded.commonground.layerType"]?.includes("process"), - handleClick: () => { - setFilters({ ...baseFilters, "embedded.nl.embedded.commonground.layerType": ["process"] }); - navigate("/components"); - }, - }, - { - label: t("Data models"), - current: - pathname === "/components" && filters["embedded.nl.embedded.commonground.layerType"]?.includes("data"), - handleClick: () => { - setFilters({ ...baseFilters, "embedded.nl.embedded.commonground.layerType": ["data"] }); - navigate("/components"); - }, - }, - { - label: t("API's"), - current: - pathname === "/components" && filters["embedded.nl.embedded.commonground.layerType"]?.includes("service"), - handleClick: () => { - setFilters({ ...baseFilters, "embedded.nl.embedded.commonground.layerType": ["service"] }); - navigate("/components"); - }, - }, - ], - }, - { - label: t("Organizations"), - current: pathname.includes("/organizations"), - handleClick: () => { - navigate("/organizations"); - }, - }, + const secondaryTopNavItemsMobile: ITopNavItem[] = [ { - label: t("Initiatives"), - current: pathname === "/components" && filters.developmentStatus === "concept", + label: t("Login"), + type: "external", + current: pathname === "/login", handleClick: () => { - setFilters({ ...baseFilters, developmentStatus: "concept" }); - navigate("/components"); + open(process.env.ADMIN_DASHBOARD_URL ?? "#"); }, - }, - { - label: "Documentatie", - current: pathname.includes("/documentation"), - subItems: [ - { - label: t("About"), - current: pathname === "/documentation/about", - handleClick: () => navigate("/documentation/about"), - }, - { - label: t("Use"), - current: pathname === "/documentation/usage", - handleClick: () => { - navigate("/documentation/usage"); - }, - }, - ], + icon: , }, ]; const secondaryTopNavItems = [ { label: t("Login"), + type: "external", current: pathname === "/login", handleClick: () => { open(process.env.ADMIN_DASHBOARD_URL ?? "#"); @@ -144,22 +61,133 @@ export const HeaderTemplate: React.FC = ({ layoutClassName }, ]; + const _useHeaderContent = useHeaderContent(); + const getHeaderContent = _useHeaderContent.getContent(); + React.useEffect(() => { + const itemsArray: ITopNavItem[] = []; + + getHeaderContent.isSuccess && + getHeaderContent.data.map((item: any) => { + const isCurrent = (current: any) => { + if (current && !current.filterCondition) { + switch (current.operator) { + case "equals": + if (process.env.GATSBY_USE_GITHUB_REPOSITORY_NAME_AS_PATH_PREFIX === "true") { + return pathname === `/${process.env.GATSBY_GITHUB_REPOSITORY_NAME}${current.pathname}`; + } else { + return pathname === current.pathname; + } + + case "includes": + return pathname.includes(current.pathname); + } + } + if (current && current.filterCondition) { + switch (current.operator) { + case "equals": + if (process.env.GATSBY_USE_GITHUB_REPOSITORY_NAME_AS_PATH_PREFIX === "true") { + return pathname === `/${process.env.GATSBY_GITHUB_REPOSITORY_NAME}${current.pathname}` && + current.filterCondition?.isObject === true + ? //@ts-ignore + filters[current.filterCondition.filter]?.includes(current.filterCondition.value) + : //@ts-ignore + filters[current.filterCondition.filter] === current.filterConditon.value; + } else { + return pathname === current.pathname && current.filterCondition?.isObject === true + ? //@ts-ignore + filters[current.filterCondition.filter]?.includes(current.filterCondition.value) + : //@ts-ignore + filters[current.filterCondition.filter] === current.filterConditon.value; + } + + case "includes": + return current.filterCondition?.isObject === true + ? pathname.includes(current.pathname) && + //@ts-ignore + filters[current.filterCondition.filter]?.includes(current.filterCondition?.value) + : pathname.includes(current.pathname) && + //@ts-ignore + filters[current.filterCondition.filter] === current.filterCondition?.value; + } + } + }; + + const getOnClick = (onClick: any, type: "readme" | "internal" | "external", label: string) => { + if (!onClick || !type || !label) return; + + if (onClick.link && !onClick.setFilter) { + if (type === "internal") { + navigate(onClick.link); + } + if (type === "external") { + open(onClick.link); + } + if (type === "readme") { + navigate(`/github/${label.replaceAll(" ", "_")}/?link=${onClick.link}`); + } + } + if (onClick.link && onClick.setFilter && type === "internal") { + onClick.setFilter?.isObject === true + ? setFilters({ ...baseFilters, [onClick.setFilter!.filter]: [onClick.setFilter!.value] }) + : setFilters({ ...baseFilters, [onClick.setFilter!.filter]: onClick.setFilter!.value }); + navigate(onClick.link); + } + }; + + const setSubItems = (subItems: ITopNavItem[]) => { + if (!subItems) return; + const subItemsArray: ITopNavItem[] = []; + + subItems.map((item: any) => { + subItemsArray.push({ + label: t(item.label), + type: item.type, + current: isCurrent(item.current), + handleClick: () => getOnClick(item.handleClick, item.type, item.label), + }); + }); + + const subItemsObject = Object.assign(subItemsArray); + + return subItemsObject; + }; + + itemsArray.push({ + label: t(item.label), + type: item.type, + current: isCurrent(item.current), + handleClick: () => getOnClick(item.handleClick, item.type, item.label), + subItems: setSubItems(item.subItems), + }); + }); + if (screenSize === "desktop") { - setTopNavItems(primaryTopNavItems); + setTopNavItems(itemsArray); + return; } - setTopNavItems([...primaryTopNavItems, ...secondaryTopNavItems]); - }, [screenSize, pathname, crumbs]); + process.env.GATSBY_HEADER_SHOW_LOGIN === "true" + ? setTopNavItems([...itemsArray, ...secondaryTopNavItemsMobile]) + : setTopNavItems(itemsArray); + }, [screenSize, pathname, crumbs, getHeaderContent]); return ( - -
- - - -
+ + {process.env.GATSBY_HEADER_SHOW_LOGIN === "true" && ( +
+ + + +
+ )}
diff --git a/pwa/static/.env.development b/pwa/static/.env.development index 8ae93ac1c..22c928b72 100644 --- a/pwa/static/.env.development +++ b/pwa/static/.env.development @@ -17,6 +17,8 @@ GATSBY_NL_DESIGN_THEME_CLASSNAME=rotterdam-theme # Header GATSBY_HEADER_LOGO_URL=https://www.rotterdam.nl/images/logo-base.svg +GATSBY_HEADER_SHOW_LOGIN="false" +GATSBY_HEADER_CONTENT=https://raw.githubusercontent.com/OpenCatalogi/web-app/348679b7537b20e51767dfdc6086349602afe219/pwa/src/templates/templateParts/header/HeaderContent.json # Footer GATSBY_FOOTER_SHOW_CREATOR="false" diff --git a/pwa/static/.env.production b/pwa/static/.env.production index c0d900230..f000830d7 100644 --- a/pwa/static/.env.production +++ b/pwa/static/.env.production @@ -14,4 +14,13 @@ GATSBY_ADMIN_DASHBOARD_URL= # Config GATSBY_NL_DESIGN_THEME_CLASSNAME= # GATSBY_GITHUB_ORGANIZATION_URL= + +# Header GATSBY_HEADER_LOGO_URL= +GATSBY_HEADER_SHOW_LOGIN= +GATSBY_HEADER_CONTENT= + +# Footer +GATSBY_FOOTER_SHOW_CREATOR= +GATSBY_FOOTER_LOGO_URL= +GATSBY_FOOTER_CONTENT=