From 2eb6455912d59bccb51d390a8bda8f939c6bc428 Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Mon, 9 Oct 2023 18:11:07 +0200 Subject: [PATCH 01/12] Create useDetailsSections hook --- .../src/components/pages/details/interface.ts | 76 +++++++++++-------- .../pages/details/useDetailsSections.tsx | 16 ++++ 2 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/pages/details/useDetailsSections.tsx diff --git a/frontend/src/components/pages/details/interface.ts b/frontend/src/components/pages/details/interface.ts index 42c9861a0..3ce7228c5 100644 --- a/frontend/src/components/pages/details/interface.ts +++ b/frontend/src/components/pages/details/interface.ts @@ -55,37 +55,47 @@ export type DetailsSectionOutdoorCourseNames = | 'touristicContent' | 'forecastWidget'; -export interface DetailsConfig { - sections: { - trek: { - name: DetailsSectionTrekNames; - display: boolean; - anchor: boolean; - order: number; - }[]; - touristicContent: { - name: DetailsSectionTouristicContentNames; - display: boolean; - anchor: boolean; - order: number; - }[]; - touristicEvent: { - name: DetailsSectionTouristicEventNames; - display: boolean; - anchor: boolean; - order: number; - }[]; - outdoorSite: { - name: DetailsSectionOutdoorSiteNames; - display: boolean; - anchor: boolean; - order: number; - }[]; - outdoorCourse: { - name: DetailsSectionOutdoorCourseNames; - display: boolean; - anchor: boolean; - order: number; - }[]; - }; +interface SectionsProps { + display: boolean; + anchor: boolean; + order: number; } + +export type SectionsTrek = SectionsProps & { + name: DetailsSectionTrekNames; +}; + +export type SectionsTouristicContent = SectionsProps & { + name: DetailsSectionTouristicContentNames; +}; + +export type SectionsTouristicEvent = SectionsProps & { + name: DetailsSectionTouristicEventNames; +}; + +export type SectionsOutdoorSite = SectionsProps & { + name: DetailsSectionOutdoorSiteNames; +}; + +export type SectionsOutdoorCourse = SectionsProps & { + name: DetailsSectionOutdoorCourseNames; +}; + +export type Sections = { + trek: SectionsTrek[]; + touristicContent: SectionsTouristicContent[]; + touristicEvent: SectionsTouristicEvent[]; + outdoorSite: SectionsOutdoorSite[]; + outdoorCourse: SectionsOutdoorCourse[]; +}; + +export type SectionsTypes = + | SectionsTrek + | SectionsTouristicContent + | SectionsTouristicEvent + | SectionsOutdoorSite + | SectionsOutdoorCourse; + +export type DetailsConfig = { + sections: Sections; +}; diff --git a/frontend/src/components/pages/details/useDetailsSections.tsx b/frontend/src/components/pages/details/useDetailsSections.tsx new file mode 100644 index 000000000..f5122afd8 --- /dev/null +++ b/frontend/src/components/pages/details/useDetailsSections.tsx @@ -0,0 +1,16 @@ +import { getDetailsConfig } from './config'; +import { Sections, SectionsTypes } from './interface'; + +export const useDetailsSections = (type: keyof Sections) => { + const { sections } = getDetailsConfig(); + + const sectionsFilteredByType = (sections[type] as SectionsTypes[]).filter( + ({ display }) => display, + ); + const anchors = sectionsFilteredByType.filter(({ anchor }) => anchor).map(({ name }) => name); + + return { + sections: sectionsFilteredByType, + anchors, + }; +}; From e50f5eb897a9c8ae4d3e60e14f551c595233a82b Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Mon, 9 Oct 2023 18:11:54 +0200 Subject: [PATCH 02/12] Use useDetailsSections in details page --- frontend/src/components/pages/details/Details.tsx | 8 +++----- frontend/src/components/pages/site/OutdoorCourseUI.tsx | 10 +++------- frontend/src/components/pages/site/OutdoorSiteUI.tsx | 10 +++------- .../pages/touristicContent/TouristicContentUI.tsx | 10 +++------- .../pages/touristicEvent/TouristicEventUI.tsx | 10 +++------- 5 files changed, 15 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/pages/details/Details.tsx b/frontend/src/components/pages/details/Details.tsx index 2b01f7220..0f711ce17 100644 --- a/frontend/src/components/pages/details/Details.tsx +++ b/frontend/src/components/pages/details/Details.tsx @@ -46,7 +46,7 @@ import { DetailsAndMapProvider } from './DetailsAndMapContext'; import { DetailsSensitiveArea } from './components/DetailsSensitiveArea'; import { useOnScreenSection } from './hooks/useHighlightedSection'; import { DetailsGear } from './components/DetailsGear'; -import { getDetailsConfig } from './config'; +import { useDetailsSections } from './useDetailsSections'; interface Props { slug: string | string[] | undefined; @@ -76,9 +76,7 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu const sectionsContainerRef = useRef(null); const hasNavigator = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - const { sections } = getDetailsConfig(); - const sectionsTrek = sections.trek.filter(({ display }) => display); - const anchors = sectionsTrek.filter(({ anchor }) => anchor === true).map(({ name }) => name); + const { sections, anchors } = useDetailsSections('trek'); useOnScreenSection({ sectionsPositions, @@ -171,7 +169,7 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu displayReservationWidget={anchors.includes('reservationWidget')} /> - {sectionsTrek.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ outdoorCourseUr const sectionsContainerRef = useRef(null); const hasNavigator = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - const { sections } = getDetailsConfig(); - const sectionsOutdoorCourse = sections.outdoorCourse.filter(({ display }) => display); - const anchors = sectionsOutdoorCourse - .filter(({ anchor }) => anchor === true) - .map(({ name }) => name); + const { sections, anchors } = useDetailsSections('outdoorCourse'); useOnScreenSection({ sectionsPositions, @@ -154,7 +150,7 @@ export const OutdoorCourseUIWithoutContext: React.FC = ({ outdoorCourseUr type={'OUTDOOR_COURSE'} /> - {sectionsOutdoorCourse.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ outdoorSiteUrl, language const sectionsContainerRef = useRef(null); const hasNavigator = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - const { sections } = getDetailsConfig(); - const sectionsOutdoorSite = sections.outdoorSite.filter(({ display }) => display); - const anchors = sectionsOutdoorSite - .filter(({ anchor }) => anchor === true) - .map(({ name }) => name); + const { sections, anchors } = useDetailsSections('outdoorSite'); useOnScreenSection({ sectionsPositions, @@ -156,7 +152,7 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language type={'OUTDOOR_SITE'} /> - {sectionsOutdoorSite.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ const isMobile = useMediaPredicate('(max-width: 1024px)'); const hasNavigator = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - const { sections } = getDetailsConfig(); - const sectionsTouristicContent = sections.touristicContent.filter(({ display }) => display); - const anchors = sectionsTouristicContent - .filter(({ anchor }) => anchor === true) - .map(({ name }) => name); + const { sections, anchors } = useDetailsSections('touristicContent'); return ( <> @@ -126,7 +122,7 @@ export const TouristicContentUI: React.FC = ({ type={'TOURISTIC_CONTENT'} /> - {sectionsTouristicContent.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ const sectionsContainerRef = useRef(null); const hasNavigator = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - const { sections } = getDetailsConfig(); - const sectionsTouristicEvents = sections.touristicEvent.filter(({ display }) => display); - const anchors = sectionsTouristicEvents - .filter(({ anchor }) => anchor === true) - .map(({ name }) => name); + const { sections, anchors } = useDetailsSections('touristicEvent'); useOnScreenSection({ sectionsPositions, @@ -153,7 +149,7 @@ export const TouristicEventUIWithoutContext: React.FC = ({ type={'TOURISTIC_EVENT'} /> - {sectionsTouristicEvents.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
Date: Tue, 17 Oct 2023 17:03:26 +0200 Subject: [PATCH 03/12] Get details template HTML available to publicRuntimeConfig --- frontend/jestAfterEnv.setup.tsx | 9 +++- frontend/src/services/getConfig.js | 78 +++++++++++++++--------------- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/frontend/jestAfterEnv.setup.tsx b/frontend/jestAfterEnv.setup.tsx index 12435013c..de38a1d21 100644 --- a/frontend/jestAfterEnv.setup.tsx +++ b/frontend/jestAfterEnv.setup.tsx @@ -226,7 +226,14 @@ setConfig({ "order": 140 } ], - } + outdoorSite: [], + outdoorCourse: [], + touristicContent: [], + touristicEvent: [] + }, + }, + detailsSectionHtml: { + forecastWidget: { default: '\n' } }, home: {}, map: {}, diff --git a/frontend/src/services/getConfig.js b/frontend/src/services/getConfig.js index 2142ba6e4..9d3887bae 100644 --- a/frontend/src/services/getConfig.js +++ b/frontend/src/services/getConfig.js @@ -2,6 +2,19 @@ const fs = require('fs'); const deepmerge = require('deepmerge'); const { getLocales } = require('./getLocales'); +function getFiles(dir, files = []) { + const fileList = fs.readdirSync(dir); + for (const file of fileList) { + const name = `${dir}/${file}`; + if (fs.statSync(name).isDirectory()) { + getFiles(name, files); + } else { + files.push(name); + } + } + return files; +} + const getContent = (path, parse) => { if (fs.existsSync(path)) { const content = fs.readFileSync(path).toString(); @@ -20,28 +33,22 @@ const getConfig = (file, parse = true, deepMerge = false) => { const merge = (elem1, elem2) => { if (Array.isArray(elem1)) { return [...elem2, ...elem1]; - } + } if (deepMerge) { - return deepmerge.all([elem2 ?? {}, elem1 ?? {}]) + return deepmerge.all([elem2 ?? {}, elem1 ?? {}]); + } else { + return { ...elem1, ...elem2 }; } - else { - return { ...elem1, ...elem2 } - }; }; - return parse - ? merge(defaultConfig, overrideConfig) - : overrideConfig || defaultConfig; + return parse ? merge(defaultConfig, overrideConfig) : overrideConfig || defaultConfig; }; const getTemplates = (file, languages) => { const [path] = file.split('.html'); return languages.reduce( (list, language) => { - list[language] = getContent( - `./customization/config/${path}-${language}.html`, - false, - ); + list[language] = getContent(`./customization/config/${path}-${language}.html`, false); return list; }, { default: getContent(`./customization/config/${file}`, false) }, @@ -55,7 +62,7 @@ const configDetails = getConfig('details.json', true, true); const filterAndOrderSectionsDetails = sections => sections .filter(({ name }, index, array) => array.findIndex(item => item.name === name) === index) - .sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) + .sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)); const details = { ...configDetails, @@ -65,35 +72,26 @@ const details = { touristicContent: filterAndOrderSectionsDetails(configDetails.sections.touristicContent), touristicEvent: filterAndOrderSectionsDetails(configDetails.sections.touristicEvent), outdoorSite: filterAndOrderSectionsDetails(configDetails.sections.outdoorSite), - outdoorCourse: filterAndOrderSectionsDetails(configDetails.sections.outdoorCourse) - } -} + outdoorCourse: filterAndOrderSectionsDetails(configDetails.sections.outdoorCourse), + }, +}; + +const detailsFiles = getFiles('./customization/html/details'); +const detailsSectionHtml = detailsFiles + .map(item => item.replace('./customization', '../')) + .reduce((list, file) => { + const [nameFile] = file.split('/').pop().split('.'); + return { ...list, [nameFile]: getTemplates(file, headers.menu.supportedLanguages) }; + }, {}); const getAllConfigs = { - homeBottomHtml: getTemplates( - '../html/homeBottom.html', - headers.menu.supportedLanguages, - ), - homeTopHtml: getTemplates( - '../html/homeTop.html', - headers.menu.supportedLanguages, - ), - headerTopHtml: getTemplates( - '../html/headerTop.html', - headers.menu.supportedLanguages, - ), - headerBottomHtml: getTemplates( - '../html/headerBottom.html', - headers.menu.supportedLanguages, - ), - footerTopHtml: getTemplates( - '../html/footerTop.html', - headers.menu.supportedLanguages, - ), - footerBottomHtml: getTemplates( - '../html/footerBottom.html', - headers.menu.supportedLanguages, - ), + homeBottomHtml: getTemplates('../html/homeBottom.html', headers.menu.supportedLanguages), + homeTopHtml: getTemplates('../html/homeTop.html', headers.menu.supportedLanguages), + headerTopHtml: getTemplates('../html/headerTop.html', headers.menu.supportedLanguages), + headerBottomHtml: getTemplates('../html/headerBottom.html', headers.menu.supportedLanguages), + footerTopHtml: getTemplates('../html/footerTop.html', headers.menu.supportedLanguages), + footerBottomHtml: getTemplates('../html/footerBottom.html', headers.menu.supportedLanguages), + detailsSectionHtml, scriptsHeaderHtml: getConfig('../html/scriptsHeader.html', false), scriptsFooterHtml: getConfig('../html/scriptsFooter.html', false), style: getConfig('../theme/style.css', false), From 7b30265d6b92a738204bae4aedb93dffee7dd96e Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Tue, 17 Oct 2023 17:07:30 +0200 Subject: [PATCH 04/12] Get details template HTML available to getDetailsConfig helper --- .../src/components/pages/details/config.ts | 29 ++++++++++++++++--- .../src/components/pages/details/interface.ts | 1 + .../components/pages/details/useDetails.tsx | 2 +- .../pages/details/useDetailsSections.tsx | 4 ++- .../pages/site/useOutdoorCourse.tsx | 2 +- .../components/pages/site/useOutdoorSite.tsx | 2 +- .../touristicContent/useTouristicContent.tsx | 2 +- .../touristicEvent/useTouristicEvent.tsx | 2 +- 8 files changed, 34 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/pages/details/config.ts b/frontend/src/components/pages/details/config.ts index c3d049c12..348a2908e 100644 --- a/frontend/src/components/pages/details/config.ts +++ b/frontend/src/components/pages/details/config.ts @@ -1,10 +1,31 @@ import getNextConfig from 'next/config'; -import { DetailsConfig } from './interface'; +import { DetailsConfig, SectionsTypes } from './interface'; -export const getDetailsConfig = (): DetailsConfig => { +export const getDetailsConfig = (language: string): DetailsConfig => { const { - publicRuntimeConfig: { details }, + publicRuntimeConfig: { details, detailsSectionHtml }, } = getNextConfig(); - return details; + const destailsSection = (sections: SectionsTypes[]) => + sections.map(item => { + if (detailsSectionHtml[item.name]) { + return { + ...item, + template: + detailsSectionHtml[item.name][language] ?? detailsSectionHtml[item.name].default, + }; + } + return item; + }); + + return { + ...details, + sections: { + outdoorCourse: destailsSection(details.sections.outdoorCourse), + outdoorSite: destailsSection(details.sections.outdoorSite), + touristicContent: destailsSection(details.sections.touristicContent), + touristicEvent: destailsSection(details.sections.touristicEvent), + trek: destailsSection(details.sections.trek), + }, + }; }; diff --git a/frontend/src/components/pages/details/interface.ts b/frontend/src/components/pages/details/interface.ts index 3ce7228c5..001d4ca21 100644 --- a/frontend/src/components/pages/details/interface.ts +++ b/frontend/src/components/pages/details/interface.ts @@ -59,6 +59,7 @@ interface SectionsProps { display: boolean; anchor: boolean; order: number; + template?: string; } export type SectionsTrek = SectionsProps & { diff --git a/frontend/src/components/pages/details/useDetails.tsx b/frontend/src/components/pages/details/useDetails.tsx index f01032c1a..98eaa405d 100644 --- a/frontend/src/components/pages/details/useDetails.tsx +++ b/frontend/src/components/pages/details/useDetails.tsx @@ -77,7 +77,7 @@ export const useDetails = ( }, ); - const { sections } = getDetailsConfig(); + const { sections } = getDetailsConfig(language); const sectionsTrek = sections.trek.filter(({ display }) => display === true); diff --git a/frontend/src/components/pages/details/useDetailsSections.tsx b/frontend/src/components/pages/details/useDetailsSections.tsx index f5122afd8..c5cd6ef0f 100644 --- a/frontend/src/components/pages/details/useDetailsSections.tsx +++ b/frontend/src/components/pages/details/useDetailsSections.tsx @@ -1,8 +1,10 @@ +import { useIntl } from 'react-intl'; import { getDetailsConfig } from './config'; import { Sections, SectionsTypes } from './interface'; export const useDetailsSections = (type: keyof Sections) => { - const { sections } = getDetailsConfig(); + const { locale } = useIntl(); + const { sections } = getDetailsConfig(locale); const sectionsFilteredByType = (sections[type] as SectionsTypes[]).filter( ({ display }) => display, diff --git a/frontend/src/components/pages/site/useOutdoorCourse.tsx b/frontend/src/components/pages/site/useOutdoorCourse.tsx index 1bea33901..3bc2ec036 100644 --- a/frontend/src/components/pages/site/useOutdoorCourse.tsx +++ b/frontend/src/components/pages/site/useOutdoorCourse.tsx @@ -37,7 +37,7 @@ export const useOutdoorCourse = ( }, ); - const { sections } = getDetailsConfig(); + const { sections } = getDetailsConfig(language); const sectionsOutdoorCourse = sections.outdoorCourse.filter(({ display }) => display === true); diff --git a/frontend/src/components/pages/site/useOutdoorSite.tsx b/frontend/src/components/pages/site/useOutdoorSite.tsx index 945d9ba50..dd42ae771 100644 --- a/frontend/src/components/pages/site/useOutdoorSite.tsx +++ b/frontend/src/components/pages/site/useOutdoorSite.tsx @@ -33,7 +33,7 @@ export const useOutdoorSite = (outdoorSiteUrl: string | string[] | undefined, la }, ); - const { sections } = getDetailsConfig(); + const { sections } = getDetailsConfig(language); const sectionsOutdoorSite = sections.outdoorSite.filter(({ display }) => display === true); diff --git a/frontend/src/components/pages/touristicContent/useTouristicContent.tsx b/frontend/src/components/pages/touristicContent/useTouristicContent.tsx index fabec9d06..74cd91c25 100644 --- a/frontend/src/components/pages/touristicContent/useTouristicContent.tsx +++ b/frontend/src/components/pages/touristicContent/useTouristicContent.tsx @@ -38,7 +38,7 @@ export const useTouristicContent = ( }, ); - const { sections } = getDetailsConfig(); + const { sections } = getDetailsConfig(language); const sectionsTouristicContent = sections.touristicEvent.filter( ({ display }) => display === true, ); diff --git a/frontend/src/components/pages/touristicEvent/useTouristicEvent.tsx b/frontend/src/components/pages/touristicEvent/useTouristicEvent.tsx index cdd98943c..f15fe3039 100644 --- a/frontend/src/components/pages/touristicEvent/useTouristicEvent.tsx +++ b/frontend/src/components/pages/touristicEvent/useTouristicEvent.tsx @@ -36,7 +36,7 @@ export const useTouristicEvent = ( }, ); - const { sections } = getDetailsConfig(); + const { sections } = getDetailsConfig(language); const sectionsTouristicEvent = sections.touristicEvent.filter(({ display }) => display === true); const { sectionsReferences, sectionsPositions, useSectionReferenceCallback } = From f7625d91bc400951c84b8f5c2c06eeddf8fb1f69 Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Tue, 17 Oct 2023 17:13:41 +0200 Subject: [PATCH 05/12] Improve HtmlParser to replace variables in template mustache --- .../src/components/HtmlParser/HtmlParser.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/HtmlParser/HtmlParser.tsx b/frontend/src/components/HtmlParser/HtmlParser.tsx index 452880ae2..8159553e6 100644 --- a/frontend/src/components/HtmlParser/HtmlParser.tsx +++ b/frontend/src/components/HtmlParser/HtmlParser.tsx @@ -1,8 +1,12 @@ import { useExternalsScripts } from 'components/Layout/useExternalScripts'; import parse, { attributesToProps, DOMNode, domToReact, Element } from 'html-react-parser'; +import { useIntl } from 'react-intl'; interface HtmlParserProps { template?: string; + id?: string; + type?: string; + cityCode?: string; } interface ParserOptionsProps { @@ -31,14 +35,22 @@ const option = ({ needsConsent, triggerConsentModal }: ParserOptionsProps) => ({ }, }); -export const HtmlParser = ({ template }: HtmlParserProps) => { +export const HtmlParser = ({ template, ...propsToReplace }: HtmlParserProps) => { const { needsConsent, triggerConsentModal } = useExternalsScripts(); + const { locale } = useIntl(); if (!template) { return null; } - return <>{parse(template, option({ needsConsent, triggerConsentModal }))}; + let nextTemplate = template; + Object.entries({ language: locale, ...propsToReplace }).forEach(([key, value]) => { + if (nextTemplate.includes(`{{ ${key} }}`)) { + nextTemplate = nextTemplate.replaceAll(`{{ ${key} }}`, value); + } + }); + + return <>{parse(nextTemplate, option({ needsConsent, triggerConsentModal }))}; }; export default HtmlParser; From aa70ae35a209f8e4920177c8ef0e8cb81051d122 Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Tue, 17 Oct 2023 17:14:16 +0200 Subject: [PATCH 06/12] Create forecastWidget.html widget --- frontend/customization/html/details/forecastWidget.html | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 frontend/customization/html/details/forecastWidget.html diff --git a/frontend/customization/html/details/forecastWidget.html b/frontend/customization/html/details/forecastWidget.html new file mode 100644 index 000000000..e2825fdac --- /dev/null +++ b/frontend/customization/html/details/forecastWidget.html @@ -0,0 +1,7 @@ + From c35747ef42dbb5594bf26f6592fc451a3738164b Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Tue, 17 Oct 2023 17:51:09 +0200 Subject: [PATCH 07/12] Display custom sections on details pages --- .../src/components/pages/details/Details.tsx | 49 +++++++++--------- .../components/pages/site/OutdoorCourseUI.tsx | 30 +++++------ .../components/pages/site/OutdoorSiteUI.tsx | 50 +++++++++---------- .../touristicContent/TouristicContentUI.tsx | 50 +++++++++---------- .../pages/touristicEvent/TouristicEventUI.tsx | 50 +++++++++---------- 5 files changed, 115 insertions(+), 114 deletions(-) diff --git a/frontend/src/components/pages/details/Details.tsx b/frontend/src/components/pages/details/Details.tsx index 0f711ce17..59d444748 100644 --- a/frontend/src/components/pages/details/Details.tsx +++ b/frontend/src/components/pages/details/Details.tsx @@ -23,6 +23,7 @@ import { cn } from 'services/utils/cn'; import { renderToStaticMarkup } from 'react-dom/server'; import { MapPin } from 'components/Icons/MapPin'; import { ImageWithLegend } from 'components/ImageWithLegend'; +import { HtmlParser } from 'components/HtmlParser'; import { DetailsPreview } from './components/DetailsPreview'; import { DetailsSection } from './components/DetailsSection'; import { DetailsDescription } from './components/DetailsDescription'; @@ -40,7 +41,6 @@ import { DetailsAdvice } from './components/DetailsAdvice'; import { DetailsChildrenSection } from './components/DetailsChildrenSection'; import { DetailsCoverCarousel } from './components/DetailsCoverCarousel'; import { DetailsReservationWidget } from './components/DetailsReservationWidget'; -import { DetailsMeteoWidget } from './components/DetailsMeteoWidget'; import { VisibleSectionProvider } from './VisibleSectionContext'; import { DetailsAndMapProvider } from './DetailsAndMapContext'; import { DetailsSensitiveArea } from './components/DetailsSensitiveArea'; @@ -263,29 +263,6 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu ); } - if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - details.cities_raw?.[0] - ) { - return ( -
- {hasNavigator && ( - - - - )} -
- ); - } - if (section.name === 'altimetricProfile' && displayAltimetricProfile === true) { return (
= ({ slug, parentId, langu ); } + // Custom HTML templates + if (section.template) { + return ( +
+ + + +
+ ); + } + return null; })} diff --git a/frontend/src/components/pages/site/OutdoorCourseUI.tsx b/frontend/src/components/pages/site/OutdoorCourseUI.tsx index 0a8fd1478..a8fcb4eab 100644 --- a/frontend/src/components/pages/site/OutdoorCourseUI.tsx +++ b/frontend/src/components/pages/site/OutdoorCourseUI.tsx @@ -19,18 +19,17 @@ import { DetailsMapDynamicComponent } from 'components/Map'; import { PageHead } from 'components/PageHead'; import { Footer } from 'components/Footer'; import { OpenMapButton } from 'components/OpenMapButton'; -import { getGlobalConfig } from 'modules/utils/api.config'; import { renderToStaticMarkup } from 'react-dom/server'; import { MapPin } from 'components/Icons/MapPin'; import useHasMounted from 'hooks/useHasMounted'; import { ImageWithLegend } from 'components/ImageWithLegend'; import { cn } from 'services/utils/cn'; +import { HtmlParser } from 'components/HtmlParser'; import { cleanHTMLElementsFromString } from '../../../modules/utils/string'; import { DetailsPreview } from '../details/components/DetailsPreview'; import { ErrorFallback } from '../search/components/ErrorFallback'; import { DetailsTopIcons } from '../details/components/DetailsTopIcons'; import { DetailsCoverCarousel } from '../details/components/DetailsCoverCarousel'; -import { DetailsMeteoWidget } from '../details/components/DetailsMeteoWidget'; import { DetailsSensitiveArea } from '../details/components/DetailsSensitiveArea'; import { useDetailsSections } from '../details/useDetailsSections'; @@ -356,25 +355,26 @@ export const OutdoorCourseUIWithoutContext: React.FC = ({ outdoorCourseUr ); } - if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - outdoorCourseContent.cities_raw?.[0] - ) { + // Custom HTML templates + if (section.template) { return (
- {hasNavigator && ( - - - - )} + + +
); } diff --git a/frontend/src/components/pages/site/OutdoorSiteUI.tsx b/frontend/src/components/pages/site/OutdoorSiteUI.tsx index 06d292dcc..5211adb0e 100644 --- a/frontend/src/components/pages/site/OutdoorSiteUI.tsx +++ b/frontend/src/components/pages/site/OutdoorSiteUI.tsx @@ -23,19 +23,18 @@ import { DetailsMapDynamicComponent } from 'components/Map'; import { PageHead } from 'components/PageHead'; import { Footer } from 'components/Footer'; import { OpenMapButton } from 'components/OpenMapButton'; -import { getGlobalConfig } from 'modules/utils/api.config'; import { renderToStaticMarkup } from 'react-dom/server'; import { MapPin } from 'components/Icons/MapPin'; import useHasMounted from 'hooks/useHasMounted'; import { ImageWithLegend } from 'components/ImageWithLegend'; import { cn } from 'services/utils/cn'; +import { HtmlParser } from 'components/HtmlParser'; import { cleanHTMLElementsFromString } from '../../../modules/utils/string'; import { useOutdoorSite } from './useOutdoorSite'; import { DetailsPreview } from '../details/components/DetailsPreview'; import { ErrorFallback } from '../search/components/ErrorFallback'; import { DetailsTopIcons } from '../details/components/DetailsTopIcons'; import { DetailsCoverCarousel } from '../details/components/DetailsCoverCarousel'; -import { DetailsMeteoWidget } from '../details/components/DetailsMeteoWidget'; import { DetailsSensitiveArea } from '../details/components/DetailsSensitiveArea'; import { DetailsAndMapProvider } from '../details/DetailsAndMapContext'; import { useDetailsSections } from '../details/useDetailsSections'; @@ -395,29 +394,6 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language ); } - if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - outdoorSiteContent.cities_raw?.[0] - ) { - return ( -
- {hasNavigator && ( - - - - )} -
- ); - } - if ( section.name === 'source' && Number(outdoorSiteContent?.source?.length) > 0 @@ -502,6 +478,30 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language ); } + // Custom HTML templates + if (section.template) { + return ( +
+ + + +
+ ); + } + return null; })} diff --git a/frontend/src/components/pages/touristicContent/TouristicContentUI.tsx b/frontend/src/components/pages/touristicContent/TouristicContentUI.tsx index 128c3e9e1..3b4e15f78 100644 --- a/frontend/src/components/pages/touristicContent/TouristicContentUI.tsx +++ b/frontend/src/components/pages/touristicContent/TouristicContentUI.tsx @@ -7,10 +7,10 @@ import { TouristicContentMapDynamicComponent } from 'components/Map'; import { PageHead } from 'components/PageHead'; import { Footer } from 'components/Footer'; import { OpenMapButton } from 'components/OpenMapButton'; -import { getGlobalConfig } from 'modules/utils/api.config'; import useHasMounted from 'hooks/useHasMounted'; import { ImageWithLegend } from 'components/ImageWithLegend'; import { cn } from 'services/utils/cn'; +import { HtmlParser } from 'components/HtmlParser'; import { useTouristicContent } from './useTouristicContent'; import { DetailsPreview } from '../details/components/DetailsPreview'; import { DetailsSection } from '../details/components/DetailsSection'; @@ -20,7 +20,6 @@ import { DetailsSource } from '../details/components/DetailsSource'; import { DetailsCoverCarousel } from '../details/components/DetailsCoverCarousel'; import { DetailsHeaderMobile, marginDetailsChild } from '../details/Details'; import { HtmlText } from '../details/utils'; -import { DetailsMeteoWidget } from '../details/components/DetailsMeteoWidget'; import { DetailsHeader } from '../details/components/DetailsHeader'; import { useDetailsSections } from '../details/useDetailsSections'; @@ -244,29 +243,6 @@ export const TouristicContentUI: React.FC = ({ ); } - if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - touristicContent.cities_raw?.[0] - ) { - return ( -
- {hasNavigator && ( - - - - )} -
- ); - } - if (section.name === 'source' && touristicContent.sources.length > 0) { return (
= ({ ); } + // Custom HTML templates + if (section.template) { + return ( +
+ + + +
+ ); + } + return null; })} diff --git a/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx b/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx index f25d4c52e..30f678cd5 100644 --- a/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx +++ b/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx @@ -18,16 +18,15 @@ import { DetailsMapDynamicComponent } from 'components/Map'; import { PageHead } from 'components/PageHead'; import { Footer } from 'components/Footer'; import { OpenMapButton } from 'components/OpenMapButton'; -import { getGlobalConfig } from 'modules/utils/api.config'; import useHasMounted from 'hooks/useHasMounted'; import { ImageWithLegend } from 'components/ImageWithLegend'; import { cn } from 'services/utils/cn'; +import { HtmlParser } from 'components/HtmlParser'; import { cleanHTMLElementsFromString } from '../../../modules/utils/string'; import { DetailsPreview } from '../details/components/DetailsPreview'; import { ErrorFallback } from '../search/components/ErrorFallback'; import { DetailsTopIcons } from '../details/components/DetailsTopIcons'; import { DetailsCoverCarousel } from '../details/components/DetailsCoverCarousel'; -import { DetailsMeteoWidget } from '../details/components/DetailsMeteoWidget'; import { useDetailsSections } from '../details/useDetailsSections'; interface Props { @@ -277,29 +276,6 @@ export const TouristicEventUIWithoutContext: React.FC = ({ ); } - if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - touristicEventContent.cities_raw?.[0] - ) { - return ( -
- {hasNavigator && ( - - - - )} -
- ); - } - if (section.name === 'source' && touristicEventContent.sources.length > 0) { return (
= ({ ); } + // Custom HTML templates + if (section.template) { + return ( +
+ + + +
+ ); + } + return null; })} From 1f4b2df9fd6502fbc974707fd7b82732999173a9 Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Tue, 17 Oct 2023 17:51:51 +0200 Subject: [PATCH 08/12] Delete DetailsMeteoWidget component and related settings --- frontend/config/global.json | 1 - .../DetailsMeteoWidget/DetailsMeteoWidget.tsx | 40 ------------------- .../components/DetailsMeteoWidget/index.ts | 1 - 3 files changed, 42 deletions(-) delete mode 100644 frontend/src/components/pages/details/components/DetailsMeteoWidget/DetailsMeteoWidget.tsx delete mode 100644 frontend/src/components/pages/details/components/DetailsMeteoWidget/index.ts diff --git a/frontend/config/global.json b/frontend/config/global.json index f8d4cdd79..b91c86d85 100644 --- a/frontend/config/global.json +++ b/frontend/config/global.json @@ -20,7 +20,6 @@ "enableReport": true, "enableSearchByMap": true, "enableServerCache": true, - "enableMeteoWidget": true, "maxLengthTrekAllowedFor3DRando": 25000, "minAltitudeDifferenceToDisplayElevationProfile": 0, "accessibilityCodeNumber": "114", diff --git a/frontend/src/components/pages/details/components/DetailsMeteoWidget/DetailsMeteoWidget.tsx b/frontend/src/components/pages/details/components/DetailsMeteoWidget/DetailsMeteoWidget.tsx deleted file mode 100644 index 8c0852326..000000000 --- a/frontend/src/components/pages/details/components/DetailsMeteoWidget/DetailsMeteoWidget.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import useHasMounted from 'hooks/useHasMounted'; -import styled from 'styled-components'; - -const Wrapper = styled.div` - position: relative; - margin: auto; - padding-bottom: 150px; /* 16:9 */ - padding-top: 25px; - height: 0; - - max-width: 100%; - margin: auto; - - & iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - } -`; - -export const DetailsMeteoWidget: React.FC<{ code: string }> = ({ code }) => { - const display = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - - if (display === false) { - return null; - } - - return ( - - - - ); -}; diff --git a/frontend/src/components/pages/details/components/DetailsMeteoWidget/index.ts b/frontend/src/components/pages/details/components/DetailsMeteoWidget/index.ts deleted file mode 100644 index 2ce9d6a79..000000000 --- a/frontend/src/components/pages/details/components/DetailsMeteoWidget/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DetailsMeteoWidget } from './DetailsMeteoWidget'; From 456584bb68a3ae5845aaac73d05521c236a6ae0b Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Tue, 17 Oct 2023 17:52:25 +0200 Subject: [PATCH 09/12] Update documentation about Templates for details page --- docs/customization.md | 110 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index e0e0a0f14..4d20fc3d0 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -266,19 +266,121 @@ You should at least override `home.title`, `home.description` and `home.welcome- ## HTML / Scripts -You can include some HTML parts in different sections of the layout application, with files: +### HTML templates : + +You can include some HTML parts in different sections of the layout application. +These templates can be translated by using the language code as a suffix (e.g. `homeTop-en.html` will be rendered only for the English interface). The application tries to find the localized template first, otherwise it tries the non-localized template, otherwise it displays nothing. +NB: If you want to display a message common to all languages but not to a particular language (e.g. french), just create the template suffixed with its language code (e.g. `-fr.html`) and leave it empty, and voilà! + +See examples in https://github.com/GeotrekCE/Geotrek-rando-v3/tree/main/frontend/customization/html. + +#### Templates available on all pages - `customization/html/headerTop.html`: before the header section - `customization/html/headerBottom.html`: after the header section and before the content page - `customization/html/footerTop.html`: before the footer section and after the content page - `customization/html/footerBottom.html`: after the footer section + +#### Templates available on home page + - `customization/html/homeTop.html`: first section of the homepage - `customization/html/homeBottom.html`: last section of the homepage -These templates can be translated by using the language code as a suffix (e.g. `homeTop-en.html` will be rendered only for the English interface). The application tries to find the localized template first, otherwise it tries the non-localized template, otherwise it displays nothing. -NB: If you want to display a message common to all languages but not to a particular language (e.g. french), just create the template suffixed with its language code (e.g. `-fr.html`) and leave it empty, and voilà! +#### Templates on details page (trek, touristic content, touristic event, outdoor site and outdoor course) + +You can create your own templates to display practical information or widgets in different parts of the details page. There are 3 steps to follow: + +1. Create a new file suffixed with `.html` in `customization/html/details/` (e.g. `example.html`) and fill the the content with html tags + + ```html +
The id of this {{ type }} is {{ id }}
+ ``` + +You can define variables in "mustache templates" (meaning between brackets `{{ variable }}`) that will be converted once rendered. For the moment, there are 4 variables available: + +- Page ID with `{{ id }}` +- Content type `{{ type }}`: rendered values are "trek", "touristicContent", "touristicEvent", "outdoorSite", "outdoorCourse"). +- The code of the (departure) city `{{ cityCode }}`: useful for widgets such as forecast. +- The language code `{{ language }}` The current language of the page. + +When choosing a template name, care must be taken not to select a reserved name used by sections defined by the application (e.g `presentation`, see https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/config/details.json). + If you do, the customized template will not be displayed. + +2. Copy the template name without the `.html` suffix into the `customization/html/details.json` file. + For example I want to display it in treks and outdoor sites details page: + ```json + { + "sections": { + "trek": [ + { + "name": "example", + "display": true, + "anchor": true, + "order": 11 + } + ], + "outdoorSite": [ + { + "name": "example", + "display": true, + "anchor": true, + "order": 11 + } + ] + } + } + ``` +3. Copy the section title/anchor into the translations files. + For example in `customization/translations/en.json`: + ```json + { + "details": { + "example": "My example" + } + } + ``` + +You can take a look at `customization/html/details/forecastWidget.html` which shows the implementation. +By default the "forecast widget" is enabled for all content types; if you want to remove it, you need to write it explicitly in the `customization/html/details.json` file. -See examples in https://github.com/GeotrekCE/Geotrek-rando-v3/tree/main/frontend/customization/html. +```json +{ + "sections": { + "trek": [ + { + "name": "forecastWidget", + "display": false + } + ], + "touristicContent": [ + { + "name": "forecastWidget", + "display": false + } + ], + "touristicEvent": [ + { + "name": "forecastWidget", + "display": false + } + ], + "outdoorSite": [ + { + "name": "forecastWidget", + "display": false + } + ], + "outdoorCourse": [ + { + "name": "forecastWidget", + "display": false + } + ] + } +} +``` + +### Scripts You can also include some scripts: From 86028e4f93e15717c7b772dbc3918a67d702ee74 Mon Sep 17 00:00:00 2001 From: Florian Sommariva Date: Fri, 20 Oct 2023 15:14:22 +0200 Subject: [PATCH 10/12] Do not display custom section if an used variable is undefined --- .../src/components/pages/details/Details.tsx | 14 ++++++++++++-- frontend/src/components/pages/details/utils.tsx | 16 ++++++++++++++++ .../components/pages/site/OutdoorCourseUI.tsx | 14 ++++++++++++-- .../src/components/pages/site/OutdoorSiteUI.tsx | 14 ++++++++++++-- .../touristicContent/TouristicContentUI.tsx | 10 ++++++++-- .../pages/touristicEvent/TouristicEventUI.tsx | 13 +++++++++++-- 6 files changed, 71 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/pages/details/Details.tsx b/frontend/src/components/pages/details/Details.tsx index 59d444748..38a941d52 100644 --- a/frontend/src/components/pages/details/Details.tsx +++ b/frontend/src/components/pages/details/Details.tsx @@ -32,7 +32,11 @@ import { DetailsCardSection } from './components/DetailsCardSection'; import { useDetails } from './useDetails'; import { ErrorFallback } from '../search/components/ErrorFallback'; import { DetailsTopIcons } from './components/DetailsTopIcons'; -import { generateTouristicContentUrl, HtmlText } from './utils'; +import { + generateTouristicContentUrl, + HtmlText, + templatesVariablesAreDefinedAndUsed, +} from './utils'; import { DetailsSource } from './components/DetailsSource'; import { DetailsInformationDesk } from './components/DetailsInformationDesk'; @@ -545,7 +549,13 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu } // Custom HTML templates - if (section.template) { + if ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: details.id.toString(), + cityCode: details.cities_raw?.[0], + }) + ) { return (
{ + if (!template) { + return false; + } + return Object.entries(variables).every( + ([key, value]) => !template.includes(`{{ ${key} }}`) || value, + ); +}; diff --git a/frontend/src/components/pages/site/OutdoorCourseUI.tsx b/frontend/src/components/pages/site/OutdoorCourseUI.tsx index a8fcb4eab..2e3fdfcd8 100644 --- a/frontend/src/components/pages/site/OutdoorCourseUI.tsx +++ b/frontend/src/components/pages/site/OutdoorCourseUI.tsx @@ -7,7 +7,11 @@ import { DetailsHeader } from 'components/pages/details/components/DetailsHeader import { DetailsSection } from 'components/pages/details/components/DetailsSection'; import { DetailsHeaderMobile, marginDetailsChild } from 'components/pages/details/Details'; import { useOnScreenSection } from 'components/pages/details/hooks/useHighlightedSection'; -import { generateTouristicContentUrl, HtmlText } from 'components/pages/details/utils'; +import { + generateTouristicContentUrl, + HtmlText, + templatesVariablesAreDefinedAndUsed, +} from 'components/pages/details/utils'; import { VisibleSectionProvider } from 'components/pages/details/VisibleSectionContext'; import { useOutdoorCourse } from 'components/pages/site/useOutdoorCourse'; import { useMemo, useRef } from 'react'; @@ -356,7 +360,13 @@ export const OutdoorCourseUIWithoutContext: React.FC = ({ outdoorCourseUr } // Custom HTML templates - if (section.template) { + if ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: outdoorCourseContent.id.toString(), + cityCode: outdoorCourseContent.cities_raw?.[0], + }) + ) { return (
= ({ outdoorSiteUrl, language } // Custom HTML templates - if (section.template) { + if ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: outdoorSiteContent.id.toString(), + cityCode: outdoorSiteContent.cities_raw?.[0], + }) + ) { return (
= ({ } // Custom HTML templates - if (section.template) { + if ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: touristicContent.id.toString(), + cityCode: touristicContent.cities_raw?.[0], + }) + ) { return (
= ({ } // Custom HTML templates - if (section.template) { + if ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: touristicEventContent.id.toString(), + cityCode: touristicEventContent.cities_raw?.[0], + }) + ) { return (
Date: Wed, 25 Oct 2023 14:34:05 +0200 Subject: [PATCH 11/12] Bump package.json / 3.16.0 --- docs/changelog.md | 10 ++++++++++ frontend/package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 880ea919f..a0068ac88 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 3.16.0 (2023-10-25) + +**✨ Improvements** + +- Define custom template sections for details pages #988 + +**📝 Documentation** + +- ⚠️ The `enableMeteoWidge` in `global.json` is no longer supported, read the doc to see how to deactivate this widget on the details page + ## 3.15.5 (2023-10-23) **🐛 Fixes** diff --git a/frontend/package.json b/frontend/package.json index 87fddce53..49c7bf130 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "geotrek-rando-frontend", - "version": "3.15.5", + "version": "3.16.0", "private": true, "scripts": { "debug": "NODE_OPTIONS='--inspect' next ./src", From 007d4e3d7e927055208cf1d4d534bbe75f3a7258 Mon Sep 17 00:00:00 2001 From: babastienne Date: Wed, 25 Oct 2023 16:15:22 +0200 Subject: [PATCH 12/12] Update docs/changelog.md --- docs/changelog.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a0068ac88..5c97e3e04 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,13 +2,13 @@ ## 3.16.0 (2023-10-25) -**✨ Improvements** +**💥 Breaking changes** -- Define custom template sections for details pages #988 +- The `enableMeteoWidget` in `global.json` is no longer supported. By default the widget is activated on all instances. [Read the doc](https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/docs/customization.md#html--scripts) to see how to deactivate this widget on the details page -**📝 Documentation** +**🚀 New features** -- ⚠️ The `enableMeteoWidge` in `global.json` is no longer supported, read the doc to see how to deactivate this widget on the details page +- Define custom template sections for details pages #988 ## 3.15.5 (2023-10-23) @@ -29,6 +29,10 @@ - Ask the user's consent to deposit cookies (#982) +**💥 Breaking changes** + +- To keep Google Analytics running (if defined by `googleAnalyticsId` in `global.json`), you need to define the new `privacyPolicyLink` key in `global.json` with the url of your privacy policy page (See #459). + **✨ Improvements** - Add the `privacyPolicyLink` key in `global.json` to define the link of privacy policy page (#982)