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),