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: 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/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 @@ + 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/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; diff --git a/frontend/src/components/pages/details/Details.tsx b/frontend/src/components/pages/details/Details.tsx index 2b01f7220..38a941d52 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'; @@ -31,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'; @@ -40,13 +45,12 @@ 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'; 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 +80,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 +173,7 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu displayReservationWidget={anchors.includes('reservationWidget')} /> - {sectionsTrek.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ 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 ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: details.id.toString(), + cityCode: details.cities_raw?.[0], + }) + ) { + return ( +
+ + + +
+ ); + } + return null; })} 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'; 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 42c9861a0..001d4ca21 100644 --- a/frontend/src/components/pages/details/interface.ts +++ b/frontend/src/components/pages/details/interface.ts @@ -55,37 +55,48 @@ 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; + template?: string; } + +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/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 new file mode 100644 index 000000000..c5cd6ef0f --- /dev/null +++ b/frontend/src/components/pages/details/useDetailsSections.tsx @@ -0,0 +1,18 @@ +import { useIntl } from 'react-intl'; +import { getDetailsConfig } from './config'; +import { Sections, SectionsTypes } from './interface'; + +export const useDetailsSections = (type: keyof Sections) => { + const { locale } = useIntl(); + const { sections } = getDetailsConfig(locale); + + const sectionsFilteredByType = (sections[type] as SectionsTypes[]).filter( + ({ display }) => display, + ); + const anchors = sectionsFilteredByType.filter(({ anchor }) => anchor).map(({ name }) => name); + + return { + sections: sectionsFilteredByType, + anchors, + }; +}; diff --git a/frontend/src/components/pages/details/utils.tsx b/frontend/src/components/pages/details/utils.tsx index ccc0b120d..f3126e124 100644 --- a/frontend/src/components/pages/details/utils.tsx +++ b/frontend/src/components/pages/details/utils.tsx @@ -153,3 +153,19 @@ export const generateDetailsUrlFromType = ( const titleWithNoSpace = convertStringForSitemap(title); return `${routes[type]}/${id}-${encodeURI(titleWithNoSpace)}${searchParams}`; }; + +export const templatesVariablesAreDefinedAndUsed = ({ + template, + ...variables +}: { + template?: string; + id: string; + cityCode?: string; +}) => { + 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 da774961a..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'; @@ -19,20 +23,19 @@ 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 { getDetailsConfig } from '../details/config'; +import { useDetailsSections } from '../details/useDetailsSections'; interface Props { outdoorCourseUrl: string | string[] | undefined; @@ -61,11 +64,7 @@ export const OutdoorCourseUIWithoutContext: React.FC = ({ 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 +153,7 @@ export const OutdoorCourseUIWithoutContext: React.FC = ({ outdoorCourseUr type={'OUTDOOR_COURSE'} /> - {sectionsOutdoorCourse.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ outdoorCourseUr ); } + // Custom HTML templates if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - outdoorCourseContent.cities_raw?.[0] + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: outdoorCourseContent.id.toString(), + cityCode: outdoorCourseContent.cities_raw?.[0], + }) ) { return (
= ({ outdoorCourseUr ref={sectionRef[section.name]} id={`details_${section.name}_ref`} > - {hasNavigator && ( - - - - )} + + +
); } diff --git a/frontend/src/components/pages/site/OutdoorSiteUI.tsx b/frontend/src/components/pages/site/OutdoorSiteUI.tsx index fa06aaa57..9b1e03e11 100644 --- a/frontend/src/components/pages/site/OutdoorSiteUI.tsx +++ b/frontend/src/components/pages/site/OutdoorSiteUI.tsx @@ -11,7 +11,11 @@ import { DetailsSection } from 'components/pages/details/components/DetailsSecti import { DetailsSource } from 'components/pages/details/components/DetailsSource'; 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 { DetailsChildrenSection } from 'components/pages/details/components/DetailsChildrenSection'; import { useMemo, useRef } from 'react'; @@ -23,22 +27,21 @@ 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 { getDetailsConfig } from '../details/config'; +import { useDetailsSections } from '../details/useDetailsSections'; interface Props { outdoorSiteUrl: string | string[] | undefined; @@ -67,11 +70,7 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ 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 +155,7 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language type={'OUTDOOR_SITE'} /> - {sectionsOutdoorSite.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ outdoorSiteUrl, language ); } - if ( - section.name === 'forecastWidget' && - getGlobalConfig().enableMeteoWidget && - outdoorSiteContent.cities_raw?.[0] - ) { - return ( -
- {hasNavigator && ( - - - - )} -
- ); - } - if ( section.name === 'source' && Number(outdoorSiteContent?.source?.length) > 0 @@ -506,6 +482,36 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language ); } + // Custom HTML templates + if ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: outdoorSiteContent.id.toString(), + cityCode: outdoorSiteContent.cities_raw?.[0], + }) + ) { + return ( +
+ + + +
+ ); + } + return null; })} 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/TouristicContentUI.tsx b/frontend/src/components/pages/touristicContent/TouristicContentUI.tsx index 23d84c6aa..7faa5d9af 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'; @@ -19,10 +19,9 @@ import { DetailsTopIcons } from '../details/components/DetailsTopIcons'; 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 { getDetailsConfig } from '../details/config'; +import { HtmlText, templatesVariablesAreDefinedAndUsed } from '../details/utils'; import { DetailsHeader } from '../details/components/DetailsHeader'; +import { useDetailsSections } from '../details/useDetailsSections'; interface TouristicContentUIProps { touristicContentUrl: string | string[] | undefined; @@ -48,11 +47,7 @@ export const TouristicContentUI: React.FC = ({ 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 +121,7 @@ export const TouristicContentUI: React.FC = ({ type={'TOURISTIC_CONTENT'} /> - {sectionsTouristicContent.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ ); } - 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 ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: touristicContent.id.toString(), + cityCode: touristicContent.cities_raw?.[0], + }) + ) { + return ( +
+ + + +
+ ); + } + return null; })} 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/TouristicEventUI.tsx b/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx index fa8e7a0ed..e6c335b7a 100644 --- a/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx +++ b/frontend/src/components/pages/touristicEvent/TouristicEventUI.tsx @@ -6,7 +6,10 @@ import { DetailsSection } from 'components/pages/details/components/DetailsSecti import { DetailsSource } from 'components/pages/details/components/DetailsSource'; import { DetailsHeaderMobile, marginDetailsChild } from 'components/pages/details/Details'; import { useOnScreenSection } from 'components/pages/details/hooks/useHighlightedSection'; -import { generateTouristicContentUrl } from 'components/pages/details/utils'; +import { + generateTouristicContentUrl, + templatesVariablesAreDefinedAndUsed, +} from 'components/pages/details/utils'; import { VisibleSectionProvider } from 'components/pages/details/VisibleSectionContext'; import { useTouristicEvent } from 'components/pages/touristicEvent/useTouristicEvent'; import { useMemo, useRef } from 'react'; @@ -18,17 +21,16 @@ 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 { getDetailsConfig } from '../details/config'; +import { useDetailsSections } from '../details/useDetailsSections'; interface Props { touristicEventUrl: string | string[] | undefined; @@ -60,11 +62,7 @@ export const TouristicEventUIWithoutContext: React.FC = ({ 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 +151,7 @@ export const TouristicEventUIWithoutContext: React.FC = ({ type={'TOURISTIC_EVENT'} /> - {sectionsTouristicEvents.map(section => { + {sections.map(section => { if (section.name === 'presentation') { return (
= ({ ); } - 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 ( + templatesVariablesAreDefinedAndUsed({ + template: section.template, + id: touristicEventContent.id.toString(), + cityCode: touristicEventContent.cities_raw?.[0], + }) + ) { + return ( +
+ + + +
+ ); + } + return null; })} 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 } = 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),