Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define custom template sections for details pages #988

Merged
merged 10 commits into from
Oct 25, 2023
110 changes: 106 additions & 4 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div>The id of this {{ type }} is {{ id }}</div>
```

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:

Expand Down
1 change: 0 additions & 1 deletion frontend/config/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"enableReport": true,
"enableSearchByMap": true,
"enableServerCache": true,
"enableMeteoWidget": true,
"maxLengthTrekAllowedFor3DRando": 25000,
"minAltitudeDifferenceToDisplayElevationProfile": 0,
"accessibilityCodeNumber": "114",
Expand Down
7 changes: 7 additions & 0 deletions frontend/customization/html/details/forecastWidget.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<iframe
id="widget_autocomplete_preview"
className="w-full"
height="150"
src="https://meteofrance.com/widget/prevision/{{ cityCode }}0"
title="Widget meteofrance"
></iframe>
9 changes: 8 additions & 1 deletion frontend/jestAfterEnv.setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,14 @@ setConfig({
"order": 140
}
],
}
outdoorSite: [],
outdoorCourse: [],
touristicContent: [],
touristicEvent: []
},
},
detailsSectionHtml: {
forecastWidget: { default: '<iframe\n id="widget_autocomplete_preview"\n className="w-full"\n height="150"\n src="https://meteofrance.com/widget/prevision/{{ cityCode }}0"\n></iframe>\n' }
},
home: {},
map: {},
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/components/HtmlParser/HtmlParser.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
69 changes: 39 additions & 30 deletions frontend/src/components/pages/details/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -76,9 +80,7 @@ export const DetailsUIWithoutContext: React.FC<Props> = ({ slug, parentId, langu
const sectionsContainerRef = useRef<HTMLDivElement>(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,
Expand Down Expand Up @@ -171,7 +173,7 @@ export const DetailsUIWithoutContext: React.FC<Props> = ({ slug, parentId, langu
displayReservationWidget={anchors.includes('reservationWidget')}
/>

{sectionsTrek.map(section => {
{sections.map(section => {
if (section.name === 'presentation') {
return (
<section
Expand Down Expand Up @@ -265,29 +267,6 @@ export const DetailsUIWithoutContext: React.FC<Props> = ({ slug, parentId, langu
);
}

if (
section.name === 'forecastWidget' &&
getGlobalConfig().enableMeteoWidget &&
details.cities_raw?.[0]
) {
return (
<section
key={section.name}
ref={sectionRef[section.name]}
id={`details_${section.name}_ref`}
>
{hasNavigator && (
<DetailsSection
htmlId="details_forecastWidget"
className={marginDetailsChild}
>
<DetailsMeteoWidget code={details.cities_raw[0]} />
</DetailsSection>
)}
</section>
);
}

if (section.name === 'altimetricProfile' && displayAltimetricProfile === true) {
return (
<section
Expand Down Expand Up @@ -569,6 +548,36 @@ export const DetailsUIWithoutContext: React.FC<Props> = ({ slug, parentId, langu
);
}

// Custom HTML templates
if (
templatesVariablesAreDefinedAndUsed({
template: section.template,
id: details.id.toString(),
cityCode: details.cities_raw?.[0],
})
) {
return (
<section
key={section.name}
ref={sectionRef[section.name]}
id={`details_${section.name}_ref`}
>
<DetailsSection
htmlId={`details_${section.name}`}
titleId={`details.${section.name}`}
className={marginDetailsChild}
>
<HtmlParser
template={section.template}
id={details.id.toString()}
type="trek"
cityCode={details.cities_raw[0]}
dtrucs marked this conversation as resolved.
Show resolved Hide resolved
/>
</DetailsSection>
</section>
);
}

return null;
})}
</div>
Expand Down

This file was deleted.

This file was deleted.

29 changes: 25 additions & 4 deletions frontend/src/components/pages/details/config.ts
Original file line number Diff line number Diff line change
@@ -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),
},
};
};
Loading
Loading