diff --git a/docs/changelog.md b/docs/changelog.md index 3d40b2b24..dd0d2a0e2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,14 +1,27 @@ # Changelog +3.5.1 (2021-11-24) +------------------ + +**🐛 Fixes** + +* Fix filter masking (#512) +* Fix crash filters with using ID instead of index (#518) + +**🚀 New features** + +* Allow to add dynamic links to information pages in footer (#516) + 3.5.0 (2021-11-14) ------------------ -To enable Outdoor module, Geotrek-admin version 2.70.0 or higher is required +* To enable Outdoor module, Geotrek-admin version 2.70.0 or higher is required +* To display images in Event module, Geotrek-admin version 2.72.0 or higher is required **🚀 New features** -* Add outdoor sites and courses in home activity bar, seach, detail pages and offline contents (#376) -* Add touristic events in home activity bar, seach, detail pages and offline contents (#389) +* Add outdoor sites and courses in home activity bar, seach, detail pages and offline contents, enabled with ``enableOutdoor`` setting (#376) +* Add touristic events in home activity bar, seach, detail pages and offline contents, enabled with ``enableTouristicEvents`` setting (#389) **✨ Improvements** diff --git a/docs/customization.md b/docs/customization.md index b29eb8b4f..c20644817 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -51,7 +51,12 @@ In json files, you can just override the primary keys you need. You have to over - `shouldDisplayText`: `true` to display the text on above the asset, `false` to hide it. -- `footer.json` to define social networks (`facebook`, `twitter`, `youtube`, `instagram` or `fallback)` and links (based on translation labels) in footer (see example in https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/customization/config/footer.json) +- In the `footer.json` file, you can define social networks, informations about your organization, and some links (see example in https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/customization/config/footer.json). + + - Social networks: `facebook`, `twitter`, `youtube`, `instagram` or `fallback`. + - Contact information such as your name, address, phone number and email. + - Links based on the key pair `label`/`url` (can be based on translation labels for multilingual) and/or the key `informationID` whose value is equal to a flatpage identifier. + - `filter.json` to define filters to hide, their order and values (see example in https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/config/filter.json). If you want to hide some of the filter, you have to override their properties with `"display": false` - `map.json` to define basemaps URL and attributions, center (y, x), default and max zoom level (see example in https://github.com/GeotrekCE/Geotrek-rando-v3/blob/main/frontend/customization/config/map.json). diff --git a/docs/installation.md b/docs/installation.md index 70e7a2d51..30ed867dd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -98,6 +98,28 @@ docker-compose pull && docker-compose down && docker-compose up -d It will download and install the latest version of Geotrek-rando. If you want to install a specific version of Geotrek-rando, you can specify it in your `.env` file, instead of `latest`. +### Manage Docker images storage on disk: + +The old images will stay on your system and use disk storage. + +To remove images without container associated, you can run `docker image prune -a`. +You can also run `docker container prune` to remove all stopped containers. Run `docker ps -a` to list all containers on your system. + +Use case: after several images built on my server to update and customize my Geotrek-rando, my `/var/lib/docker/vfs` folder had a size of 81 Go! Identified with `sudo du -sh /var/lib/docker/vfs` command. After running `docker container prune` its size was reduced to 14 Go. And after running `docker image prune -a` its size was 7 Go. + +See https://docs.docker.com/config/pruning/ for more details about cleaning unused Dockers objects. + +Another method: If you notice a unexpectedly large amount of images remaining on your system when asking Docker for images with the command `docker images -a` (showing all the otherwise hidden intermediate images), you can start from a clean slate and delete all the existing docker images on your system by running: +`docker rmi $(docker images -a -q) -f`. +Docker supports subqueries like this one, let's understand it step by step: + +- `docker rmi` is the command to delete an image +- `$()` defines the subquery + - `docker images` list images + - `-a` (all) specifies that you want to see all of them even the intermediate ones + - `-q` (quiet) specifies that you only need to get the images IDs +- `-f` (force) means you want to bypass docker security preventing you to delete used images + # Install without Docker (not recommended) If you can't install Docker for some reason, there is also a way to directly deploy the node server to your machines. diff --git a/frontend/customization/config/filter.json b/frontend/customization/config/filter.json index fe51488c7..5c99ad834 100644 --- a/frontend/customization/config/filter.json +++ b/frontend/customization/config/filter.json @@ -1 +1,14 @@ -[] +[ + { + "id": "cities", + "display": false + }, + { + "id": "structures", + "display": false + }, + { + "id": "difficulty", + "display": false + } +] diff --git a/frontend/customization/config/footer.json b/frontend/customization/config/footer.json index 1e2a3785f..2394b8c66 100644 --- a/frontend/customization/config/footer.json +++ b/frontend/customization/config/footer.json @@ -4,6 +4,9 @@ { "id": "twitter", "url": "https://twitter.com/pnecrins" } ], "links": [ + { + "informationID": 4 + }, { "label": "footer.links", "url": "https://www.ecrins-parcnational.fr/liens-internet" }, { "label": "footer.access", "url": "https://www.ecrins-parcnational.fr/venir-dans-les-ecrins" }, { diff --git a/frontend/package.json b/frontend/package.json index 2d3a74cdd..c6dcccb8f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "geotrek-rando-frontend", - "version": "3.5.0", + "version": "3.5.1", "private": true, "scripts": { "debug": "NODE_OPTIONS='--inspect' next ./src", diff --git a/frontend/src/components/Footer/PortalLinks/PortalLinks.tsx b/frontend/src/components/Footer/PortalLinks/PortalLinks.tsx index 4beb9430c..b12dc9e60 100644 --- a/frontend/src/components/Footer/PortalLinks/PortalLinks.tsx +++ b/frontend/src/components/Footer/PortalLinks/PortalLinks.tsx @@ -1,14 +1,14 @@ -import { Link } from 'components/Link'; +import NextLink from 'next/link'; import { Plus } from 'components/Icons/Plus'; import { Minus } from 'components/Icons/Minus'; import { FormattedMessage } from 'react-intl'; import { usePortalLinks } from './usePortalLinks'; -import { PortalLink } from '../interface'; +import { PortalLinkStatic } from '../interface'; import { isLinkInternal, linkWithoutHost } from '../utils'; interface PortalLinksContentProps { className?: string; - links: PortalLink[]; + links: PortalLinkStatic[]; } export interface PortalLinksProps extends PortalLinksContentProps { @@ -92,16 +92,16 @@ const PortalLinksMobileContent: React.FC = ({ className ); }; -const PortalLinkRendered: React.FC<{ link: PortalLink }> = ({ link }) => { +const PortalLinkRendered: React.FC<{ link: PortalLinkStatic }> = ({ link }) => { const className = 'text-greySoft text-Mobile-C3 desktop:text-P1 cursor-pointer hover:text-white transition-all desktop:text-right'; if (typeof window !== 'undefined') { return isLinkInternal(link.url) ? ( - -
+ + -
- + + ) : ( diff --git a/frontend/src/components/Footer/interface.ts b/frontend/src/components/Footer/interface.ts index b330bb488..89e67e993 100644 --- a/frontend/src/components/Footer/interface.ts +++ b/frontend/src/components/Footer/interface.ts @@ -11,13 +11,25 @@ export interface PortalContact { mail: string; } -export interface PortalLink { +export interface PortalLinkStatic { label: string; url: string; } -export interface FooterConfig { +interface PortalLinkDynamic { + informationID: number; +} + +export type PortalLink = PortalLinkStatic | PortalLinkDynamic; + +export interface FooterConfigInput { socialNetworks?: SocialNetwork[]; links?: PortalLink[]; contact?: Partial; } + +export interface FooterConfigOutput { + socialNetworks?: SocialNetwork[]; + links?: PortalLinkStatic[]; + contact?: Partial; +} diff --git a/frontend/src/components/Footer/useFooter.ts b/frontend/src/components/Footer/useFooter.ts index a549003a7..ab150dcba 100644 --- a/frontend/src/components/Footer/useFooter.ts +++ b/frontend/src/components/Footer/useFooter.ts @@ -1,7 +1,12 @@ import getNextConfig from 'next/config'; -import { FooterConfig } from './interface'; +import { useRouter } from 'next/router'; +import { useQuery } from 'react-query'; +import { getFlatPages } from 'modules/flatpage/connector'; +import { MenuItem } from 'modules/header/interface'; +import { getDefaultLanguage } from 'modules/header/utills'; +import { FooterConfigInput, FooterConfigOutput, PortalLinkStatic } from './interface'; -const getFooterConfig = (): FooterConfig => { +const getFooterConfig = (): FooterConfigInput => { const { publicRuntimeConfig: { footer }, } = getNextConfig(); @@ -9,7 +14,29 @@ const getFooterConfig = (): FooterConfig => { return footer; }; -export const useFooter = (): { config: FooterConfig } => { - const config = getFooterConfig(); - return { config }; +export const useFooter = (): { config: FooterConfigOutput } => { + const { links, ...rest } = getFooterConfig(); + let nextLinks; + // If the footer config contains `informationID` keys,the app retrieves "flatpages" to get the corresponding label/url + if (links && links.some(link => 'informationID' in link)) { + const language = useRouter().locale ?? getDefaultLanguage(); + const { data } = useQuery(['header', language], () => + getFlatPages(language), + ); + nextLinks = links + .map(link => { + if ('informationID' in link) { + const page = data?.find(({ id }) => id === link.informationID); + if (page) { + return { label: page.title, url: page.url }; + } + return null; + } + return link; + }) + // If the informationID doesn't match with any flatPage id, it won't be displayed + .filter(Boolean) as PortalLinkStatic[]; + } + + return { config: { links: nextLinks ?? (links as PortalLinkStatic[]), ...rest } }; }; diff --git a/frontend/src/components/Footer/utils.tsx b/frontend/src/components/Footer/utils.tsx index 6b45f83d7..5f388466b 100644 --- a/frontend/src/components/Footer/utils.tsx +++ b/frontend/src/components/Footer/utils.tsx @@ -1,12 +1,9 @@ export const isLinkInternal = (urlString: string): boolean => { - if (typeof window !== 'undefined') { - const url = new URL(urlString); - return url.hostname === window.location.hostname; - } - return false; + const { hostname } = new URL(urlString, document.baseURI); + return hostname === global.location.hostname; }; export const linkWithoutHost = (urlString: string): string => { - const url = new URL(urlString); - return url.pathname; + const { pathname } = new URL(urlString, document.baseURI); + return pathname; }; diff --git a/frontend/src/components/Header/BurgerMenu/__tests__/BurgerMenu.test.tsx b/frontend/src/components/Header/BurgerMenu/__tests__/BurgerMenu.test.tsx index ecef9be9b..d5318fe7c 100644 --- a/frontend/src/components/Header/BurgerMenu/__tests__/BurgerMenu.test.tsx +++ b/frontend/src/components/Header/BurgerMenu/__tests__/BurgerMenu.test.tsx @@ -17,16 +17,19 @@ test('AAU, I can see a BurgerMenu', () => { title: 'NationalPark', url: 'https://www.ecrins-parcnational.fr/', order: 1, + id: 2, }, { title: 'Maisons du Parc', url: 'https://www.ecrins-parcnational.fr/', order: 2, + id: 1, }, { title: 'Informations utiles', url: 'https://www.ecrins-parcnational.fr/', order: 3, + id: 3, }, ]} />, diff --git a/frontend/src/modules/filters/utils.ts b/frontend/src/modules/filters/utils.ts index 12f0b8fc2..95405e7a2 100644 --- a/frontend/src/modules/filters/utils.ts +++ b/frontend/src/modules/filters/utils.ts @@ -212,11 +212,15 @@ export const computeFiltersToDisplay = ({ outdoorRatingScale: OutdoorRatingScale[]; outdoorPractice: OutdoorPracticeChoices; }): FilterState[] => { - const currentNumberOfPracticeOptionsSelected = currentFiltersState[0].selectedOptions.length; + const trekPracticeFilter = currentFiltersState.find(i => i.id === PRACTICE_ID); + const touristicContentFilter = currentFiltersState.find(i => i.id === CATEGORY_ID); + const outdoorPracticeFilter = currentFiltersState.find(i => i.id === OUTDOOR_ID); + + const currentNumberOfPracticeOptionsSelected = trekPracticeFilter?.selectedOptions.length ?? 0; const currentNumberOfTouristicContentOptionsSelected = - currentFiltersState[1].selectedOptions.length; + touristicContentFilter?.selectedOptions.length ?? 0; const currentNumberOfOutdoorPraticeOptionsSelected = - currentFiltersState[6].selectedOptions.length; + outdoorPracticeFilter?.selectedOptions.length ?? 0; const filtersToAdd: FilterState[][] = []; @@ -227,7 +231,7 @@ export const computeFiltersToDisplay = ({ } // Services filters if (currentNumberOfTouristicContentOptionsSelected > 0 || selectedFilterId === CATEGORY_ID) { - currentFiltersState[1].selectedOptions.forEach(selectedOptions => { + touristicContentFilter?.selectedOptions.forEach(selectedOptions => { filtersToAdd.push( getTypesFiltersState({ serviceId: selectedOptions.value, @@ -238,7 +242,7 @@ export const computeFiltersToDisplay = ({ } // Outdoor filters if (currentNumberOfOutdoorPraticeOptionsSelected > 0 || selectedFilterId === OUTDOOR_ID) { - currentFiltersState[6].selectedOptions.forEach(selectedOptions => { + outdoorPracticeFilter?.selectedOptions.forEach(selectedOptions => { filtersToAdd.push( getOutdoorRatingFiltersState({ practiceId: selectedOptions.value, diff --git a/frontend/src/modules/flatpage/adapter.ts b/frontend/src/modules/flatpage/adapter.ts index f50dc2a94..73f084d59 100644 --- a/frontend/src/modules/flatpage/adapter.ts +++ b/frontend/src/modules/flatpage/adapter.ts @@ -10,6 +10,7 @@ const adaptFlatPageToMenuItem = (rawFlatPage: RawFlatPage): MenuItem => ({ : generateFlatPageUrl(rawFlatPage.id, rawFlatPage.title), title: rawFlatPage.title, order: rawFlatPage.order, + id: rawFlatPage.id, }); export const adaptFlatPages = (rawFlatPages: RawFlatPage[]): MenuItem[] => { diff --git a/frontend/src/modules/header/interface.ts b/frontend/src/modules/header/interface.ts index 8adeb5f7c..46703055b 100644 --- a/frontend/src/modules/header/interface.ts +++ b/frontend/src/modules/header/interface.ts @@ -1,10 +1,12 @@ export interface MenuItem { + id: number; title: string; url: string; order: number | null; } export interface OrderableMenuItem { + id: number; title: string; url: string; order: number; diff --git a/frontend/src/services/getConfig.js b/frontend/src/services/getConfig.js index 5e92d6341..91d5e3ffd 100644 --- a/frontend/src/services/getConfig.js +++ b/frontend/src/services/getConfig.js @@ -18,7 +18,7 @@ const getConfig = (file, parse = true) => { } const merge = (elem1, elem2) => { - if (Array.isArray(elem1)) return [...elem1, ...elem2]; + if (Array.isArray(elem1)) return [...elem2, ...elem1]; else return { ...elem1, ...elem2 }; };