From 46843c2933b7c5c2accb9fafc17dd5fcb1fef468 Mon Sep 17 00:00:00 2001 From: Antoine Giret Date: Fri, 8 Mar 2024 18:28:24 +0100 Subject: [PATCH] feat: add trip page --- gatsby-node.ts | 44 ++++++ src/fixtures/index.ts | 1 + src/fixtures/trips/22-lav/index.ts | 80 +++++++++++ src/fixtures/trips/23-velo-francette/index.ts | 83 ++++++++++- src/fixtures/trips/23-velodyssee/index.ts | 73 ++++++++++ src/fixtures/trips/index.ts | 4 +- src/fixtures/trips/trip.ts | 82 ++++++++++- src/layout/index.tsx | 4 +- src/pages/components/card.tsx | 39 ++---- src/pages/components/list.tsx | 3 +- src/pages/components/map.tsx | 14 +- src/pages/components/themes.tsx | 23 ++++ src/pages/index.tsx | 6 +- src/pages/trip/index.tsx | 129 ++++++++++++++++++ src/pages/trip/map.tsx | 83 +++++++++++ src/pages/trip/step.tsx | 74 ++++++++++ src/theme.ts | 4 +- 17 files changed, 690 insertions(+), 56 deletions(-) create mode 100644 gatsby-node.ts create mode 100644 src/fixtures/index.ts create mode 100644 src/pages/components/themes.tsx create mode 100644 src/pages/trip/index.tsx create mode 100644 src/pages/trip/map.tsx create mode 100644 src/pages/trip/step.tsx diff --git a/gatsby-node.ts b/gatsby-node.ts new file mode 100644 index 0000000..936150f --- /dev/null +++ b/gatsby-node.ts @@ -0,0 +1,44 @@ +/** + * Implement Gatsby's Node APIs in this file. + * + * See: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/ + */ + +import path from 'path'; + +import { CreatePagesArgs } from 'gatsby'; + +import { trips } from './src/fixtures/trips'; + +export async function createPages({ actions, graphql }: CreatePagesArgs) { + const { createPage } = actions; + + const { data } = await graphql(` + query CreatePages { + allFile { + nodes { + id + relativePath + } + } + } + `); + + trips.forEach(({ key, coverImage, steps }, tripIndex) => { + const stepPhotosPaths = steps.flatMap(({ photos }) => photos.flatMap(({ path }) => path)); + const files = + data?.allFile.nodes.filter( + ({ relativePath }) => relativePath === coverImage || stepPhotosPaths.includes(relativePath), + ) || []; + + createPage({ + path: `/${key}`, + component: path.resolve(`src/pages/trip/index.tsx`), + context: { + tripIndex, + queryImages: files.length > 0, + imageIds: files.map(({ id }) => id), + }, + }); + }); +} diff --git a/src/fixtures/index.ts b/src/fixtures/index.ts new file mode 100644 index 0000000..14fcae6 --- /dev/null +++ b/src/fixtures/index.ts @@ -0,0 +1 @@ +export * from './trips'; diff --git a/src/fixtures/trips/22-lav/index.ts b/src/fixtures/trips/22-lav/index.ts index 6fe087d..b27f49b 100644 --- a/src/fixtures/trips/22-lav/index.ts +++ b/src/fixtures/trips/22-lav/index.ts @@ -10,6 +10,7 @@ export const lav22Trip: TTrip = { key: 'lav22', title: 'Loire à Vélo', coverImage: 'trips/22-lav/cover.jpg', + coverImageDescription: 'La Loire, Saumur et son château', color: '#174589', themes: ['nature', 'heritage', 'gastronomy'], from: 'Tours', @@ -23,6 +24,25 @@ export const lav22Trip: TTrip = { date: new Date('2022-06-13'), distance: 42.8, geometry: step1, + photos: [ + { + path: 'trips/22-lav/step-1/PXL_20220613_091927509.MP.jpg', + description: 'Un panneau de direction de la Loire à Vélo', + }, + { + path: 'trips/22-lav/step-1/PXL_20220613_093615601.jpg', + description: 'La confluence de la Loire et du Cher', + }, + { + path: 'trips/22-lav/step-1/PXL_20220613_115513025.jpg', + description: "Petite pause sieste près d'un lac à Langeais", + }, + { + path: 'trips/22-lav/step-1/PXL_20220613_162505397.jpg', + description: + "Le château d'Ussé, aussi connu comme étant le château de la Belle au Bois Dormant", + }, + ], }, { from: 'Rigny-Ussé', @@ -30,6 +50,28 @@ export const lav22Trip: TTrip = { date: new Date('2022-06-14'), distance: 43.7, geometry: step2, + photos: [ + { + path: 'trips/22-lav/step-2/PXL_20220614_084617076.jpg', + description: 'Le village de Candes-Saint-Martin, un des plus beaux de France', + }, + { + path: 'trips/22-lav/step-2/PXL_20220614_091354350.jpg', + description: 'La confluence de la Loire et de la Vienne', + }, + { + path: 'trips/22-lav/step-2/PXL_20220614_121336592.jpg', + description: "L'abbaye Royale de Fontevraud", + }, + { + path: 'trips/22-lav/step-2/PXL_20220614_135613603.jpg', + description: 'La Loire et ses larges bandes de sable', + }, + { + path: 'trips/22-lav/step-2/PXL_20220614_152957352.jpg', + description: 'La Loire, Saumur et son château', + }, + ], }, { from: 'Saumur', @@ -37,6 +79,20 @@ export const lav22Trip: TTrip = { date: new Date('2022-06-15'), distance: 59.8, geometry: step3, + photos: [ + { + path: 'trips/22-lav/step-3/PXL_20220615_090819541.jpg', + description: 'Le château de Saumur', + }, + { + path: 'trips/22-lav/step-3/PXL_20220615_091223813.MP.jpg', + description: 'La Loire vue depuis le château de Saumur', + }, + { + path: 'trips/22-lav/step-3/PXL_20220615_154149422.MP.jpg', + description: "Un bac à chaîne sur L'Authion, à Trélazé", + }, + ], }, { from: 'Angers', @@ -44,6 +100,20 @@ export const lav22Trip: TTrip = { date: new Date('2022-06-16'), distance: 70.2, geometry: step4, + photos: [ + { + path: 'trips/22-lav/step-4/PXL_20220616_075321827.jpg', + description: 'La Loire, Angers et son château', + }, + { + path: 'trips/22-lav/step-4/PXL_20220616_090427991.jpg', + description: 'Une ruelle du village de Béhuard, classé village de caractère', + }, + { + path: 'trips/22-lav/step-4/PXL_20220616_160424769.jpg', + description: "Le château d'Ancenis", + }, + ], }, { from: 'Ancenis', @@ -51,6 +121,16 @@ export const lav22Trip: TTrip = { date: new Date('2022-06-17'), distance: 42.8, geometry: step5, + photos: [ + { + path: 'trips/22-lav/step-5/PXL_20220617_092234575.MP.jpg', + description: 'Le château des Ducs de Bretagne à Nantes', + }, + { + path: 'trips/22-lav/step-5/PXL_20220617_100633964.MP.jpg', + description: 'La cour du château des Ducs de Bretagne à Nantes', + }, + ], }, ], }; diff --git a/src/fixtures/trips/23-velo-francette/index.ts b/src/fixtures/trips/23-velo-francette/index.ts index 6df7144..f59985a 100644 --- a/src/fixtures/trips/23-velo-francette/index.ts +++ b/src/fixtures/trips/23-velo-francette/index.ts @@ -10,6 +10,7 @@ export const veloFrancette23Trip: TTrip = { key: 'veloFrancette23', title: 'Vélo Francette', coverImage: 'trips/23-velo-francette/cover.jpg', + coverImageDescription: 'Le Marais Poitevin ou coulée verte', color: '#e5004b', themes: ['nature'], from: 'Chinon', @@ -23,6 +24,20 @@ export const veloFrancette23Trip: TTrip = { geometry: step1, from: 'Chinon', to: 'Thouars', + photos: [ + { + path: 'trips/23-velo-francette/step-1/PXL_20230520_102916760.jpg', + description: 'Notre chargement avant le départ', + }, + { + path: 'trips/23-velo-francette/step-1/PXL_20230520_120447928.jpg', + description: 'Des vignes près de Chinon', + }, + { + path: 'trips/23-velo-francette/step-1/PXL_20230520_140619639.jpg', + description: 'La ville de Thouars et son château', + }, + ], }, { date: new Date('2023-05-21'), @@ -30,6 +45,24 @@ export const veloFrancette23Trip: TTrip = { geometry: step2, from: 'Thouars', to: 'Parthenay', + photos: [ + { + path: 'trips/23-velo-francette/step-2/PXL_20230521_124858869.MP.jpg', + description: 'Une voie verte entre Thouars et Parthenay', + }, + { + path: 'trips/23-velo-francette/step-2/PXL_20230521_132328075.jpg', + description: 'Une rue de la ville de Parthenay', + }, + { + path: 'trips/23-velo-francette/step-2/PXL_20230521_134522260.jpg', + description: 'Vue sur Parthenay depuis son château', + }, + { + path: 'trips/23-velo-francette/step-2/PXL_20230521_141818306.jpg', + description: 'La ville de Parthenay et ses ruines', + }, + ], }, { date: new Date('2023-05-22'), @@ -37,14 +70,62 @@ export const veloFrancette23Trip: TTrip = { geometry: step3, from: 'Parthenay', to: 'Niort', + photos: [ + { + path: 'trips/23-velo-francette/step-3/PXL_20230522_082022913.jpg', + description: 'Passage sur un ponton', + }, + { + path: 'trips/23-velo-francette/step-3/PXL_20230522_092527795.MP.jpg', + description: 'Une voie verte entre Parthenay et Niort', + }, + ], + }, + { + date: new Date('2023-05-23'), + distance: 65, + geometry: step4, + from: 'Niort', + to: 'Marans', + photos: [ + { + path: 'trips/23-velo-francette/step-4/PXL_20230523_101540418.jpg', + description: 'Pont étroit en sortie de Niort', + }, + { + path: 'trips/23-velo-francette/step-4/PXL_20230523_125411619.MP.jpg', + description: 'La Sèvre Niortaise, au cœur du Marais Poitevin ou coulée verte', + }, + { + path: 'trips/23-velo-francette/step-4/PXL_20230523_130558353.jpg', + description: 'Une écluse sur la Sèvre Niortaise', + }, + { + path: 'trips/23-velo-francette/step-4/PXL_20230523_130843494.jpg', + description: 'La Sèvre Niortaise, au cœur du Marais Poitevin ou coulée verte', + }, + ], }, - { date: new Date('2023-05-23'), distance: 65, geometry: step4, from: 'Niort', to: 'Marans' }, { date: new Date('2023-05-24'), distance: 27.6, geometry: step5, from: 'Marans', to: 'La Rochelle', + photos: [ + { + path: 'trips/23-velo-francette/step-5/PXL_20230524_085147080.jpg', + description: 'La ville de Marans', + }, + { + path: 'trips/23-velo-francette/step-5/PXL_20230524_124556478.jpg', + description: 'Les tours sur le port de La Rochelle', + }, + { + path: 'trips/23-velo-francette/step-5/PXL_20230526_094008720.jpg', + description: "Reconstitution d'un bateau d'époque coloniale sur le port de La Rochelle", + }, + ], }, ], }; diff --git a/src/fixtures/trips/23-velodyssee/index.ts b/src/fixtures/trips/23-velodyssee/index.ts index ffbf71b..ed86118 100644 --- a/src/fixtures/trips/23-velodyssee/index.ts +++ b/src/fixtures/trips/23-velodyssee/index.ts @@ -10,6 +10,7 @@ export const velodyssee23Trip: TTrip = { key: 'velodyssee23', title: 'La Vélodyssée', coverImage: 'trips/23-velodyssee/cover.jpg', + coverImageDescription: "Le passage du Gois, entre l'île de Noirmoutier et le continent", color: '#f59c00', themes: ['nature', 'littoral'], from: 'Luçon', @@ -23,6 +24,20 @@ export const velodyssee23Trip: TTrip = { geometry: step1, from: 'Luçon', to: 'Jard-sur-Mer', + photos: [ + { + path: 'trips/23-velodyssee/step-1/PXL_20230526_105648489.jpg', + description: 'Notre chargement dans le train avant le départ', + }, + { + path: 'trips/23-velodyssee/step-1/PXL_20230526_134730054.jpg', + description: 'Une voie verte entre Luçon et Jard-sur-Mer', + }, + { + path: 'trips/23-velodyssee/step-1/PXL_20230526_150931600.jpg', + description: 'Le port de Saint-Vincent-sur-Jard', + }, + ], }, { date: new Date('2023-05-30'), @@ -30,6 +45,16 @@ export const velodyssee23Trip: TTrip = { geometry: step2, from: 'Jard-sur-Mer', to: 'Saint-Hilaire-de-Riez', + photos: [ + { + path: 'trips/23-velodyssee/step-2/PXL_20230530_090827504.jpg', + description: "Vue sur l'océan entre Jard-sur-Mer et Saint-Hilaire-de-Riez", + }, + { + path: 'trips/23-velodyssee/step-2/PXL_20230530_115021575.jpg', + description: 'Plage entre Jard-sur-Mer et Saint-Hilaire-de-Riez', + }, + ], }, { date: new Date('2023-05-31'), @@ -37,6 +62,32 @@ export const velodyssee23Trip: TTrip = { geometry: step3, from: 'Saint-Hilaire-de-Riez', to: 'La Guérinière', + photos: [ + { + path: 'trips/23-velodyssee/step-3/PXL_20230531_133733012.jpg', + description: 'Voie verte dans une forêt entre Saint-Hilaire-de-Riez et la Guérinière', + }, + { + path: 'trips/23-velodyssee/step-3/PXL_20230531_134247013.jpg', + description: "Pont de l'île de Noirmoutier", + }, + { + path: 'trips/23-velodyssee/step-3/PXL_20230531_135525145.MP.jpg', + description: "Traversée du pont de l'île de Noirmoutier", + }, + { + path: 'trips/23-velodyssee/step-3/PXL_20230531_140338874.jpg', + description: "Un oiseau dans les marais sur l'île de Noirmoutier", + }, + { + path: 'trips/23-velodyssee/step-3/PXL_20230531_141557573.jpg', + description: "Passage entre océan et marais l'île de Noirmoutier", + }, + { + path: 'trips/23-velodyssee/step-3/PXL_20230531_170126479.jpg', + description: "Marais sur l'île de Noirmoutier", + }, + ], }, { date: new Date('2023-06-01'), @@ -44,6 +95,28 @@ export const velodyssee23Trip: TTrip = { geometry: step4, from: 'La Guérinière', to: 'Pornic', + photos: [ + { + path: 'trips/23-velodyssee/step-4/PXL_20230601_071221189.MP.jpg', + description: 'Traversée du passage du Gois', + }, + { + path: 'trips/23-velodyssee/step-4/PXL_20230601_072115328.jpg', + description: 'Le passage du Gois', + }, + { + path: '/trips/23-velodyssee/step-4/PXL_20230601_084027133.jpg', + description: "Un port coloré d'ostréiculteurs", + }, + { + path: 'trips/23-velodyssee/step-4/PXL_20230601_121210461.jpg', + description: 'Le port de Pornic', + }, + { + path: 'trips/23-velodyssee/step-4/PXL_20230601_171121353.jpg', + description: 'Une pêcherie près de Pornic', + }, + ], }, { date: new Date('2023-06-02'), diff --git a/src/fixtures/trips/index.ts b/src/fixtures/trips/index.ts index e970638..064152b 100644 --- a/src/fixtures/trips/index.ts +++ b/src/fixtures/trips/index.ts @@ -1,10 +1,12 @@ import { lav22Trip } from './22-lav'; import { veloFrancette23Trip } from './23-velo-francette'; import { velodyssee23Trip } from './23-velodyssee'; -import { Trip } from './trip'; +import { type TTripTheme, Trip, tripThemesMap } from './trip'; export const trips: Trip[] = [ new Trip(lav22Trip), new Trip(veloFrancette23Trip), new Trip(velodyssee23Trip), ]; + +export { type TTripTheme, Trip, tripThemesMap }; diff --git a/src/fixtures/trips/trip.ts b/src/fixtures/trips/trip.ts index 09ab078..deed55d 100644 --- a/src/fixtures/trips/trip.ts +++ b/src/fixtures/trips/trip.ts @@ -1,4 +1,5 @@ import simplify from '@turf/simplify'; +import { LngLatBounds } from 'maplibre-gl'; export type TTripTheme = 'gastronomy' | 'heritage' | 'littoral' | 'nature'; @@ -14,11 +15,13 @@ export type TTripStep = { distance: number; from: string; geometry: GeoJSON.LineString; + photos?: Array<{ path: string; description: string }>; } & ({ to: string } | { isLoop: boolean }); export type TTrip = { color: string; coverImage: string; + coverImageDescription: string; description: string; from: string; key: string; @@ -27,22 +30,62 @@ export type TTrip = { themes: TTripTheme[]; } & ({ to: string } | { isLoop: boolean }); +export class TripStep { + public readonly bounds: LngLatBounds; + public readonly date: Date; + public readonly distance: number; + public readonly from: string; + public readonly geometry: GeoJSON.LineString; + public readonly isLoop: boolean; + public readonly photos: Array<{ path: string; description: string }>; + public readonly simplifiedGeometry: GeoJSON.LineString; + public readonly to: string; + + constructor({ date, distance, from, geometry, photos, ...props }: TTripStep) { + this.date = date; + this.distance = distance; + this.from = from; + this.geometry = geometry; + this.photos = photos || []; + + this.isLoop = 'isLoop' in props && props.isLoop; + this.to = 'to' in props ? props.to : this.from; + + this.simplifiedGeometry = simplify(this.geometry, { tolerance: 0.01, highQuality: false }); + + const positions = this.simplifiedGeometry.coordinates.flatMap(([lng, lat]) => ({ lat, lng })); + this.bounds = positions.slice(1).reduce( + (res, position) => { + return res.extend(position); + }, + new LngLatBounds(positions[0], positions[0]), + ); + } + + get title(): string { + return this.isLoop ? `${this.from} <> ${this.from}` : `${this.from} > ${this.to}`; + } +} + export class Trip { + public readonly bounds: LngLatBounds; public readonly color: string; public readonly coverImage: string; + public readonly coverImageDescription: string; public readonly description: string; public readonly from: string; public readonly isLoop: boolean; public readonly key: string; public readonly title: string; public readonly to: string; - public readonly simplifiedGeometry: GeoJSON.LineString; - public readonly steps: TTripStep[]; + public readonly simplifiedGeometry: GeoJSON.MultiLineString; + public readonly steps: TripStep[]; public readonly themes: TTripTheme[]; constructor({ color, coverImage, + coverImageDescription, description, from, key, @@ -53,18 +96,45 @@ export class Trip { }: TTrip) { this.color = color; this.coverImage = coverImage; + this.coverImageDescription = coverImageDescription; this.description = description; this.from = from; this.isLoop = 'isLoop' in props && props.isLoop; this.key = key; this.title = title; this.to = 'to' in props ? props.to : this.from; - this.steps = steps; + this.steps = steps.map((props) => new TripStep(props)); this.themes = themes; - this.simplifiedGeometry = simplify( - { type: 'LineString', coordinates: steps.flatMap(({ geometry }) => geometry.coordinates) }, - { tolerance: 0.01, highQuality: false }, + this.simplifiedGeometry = { + type: 'MultiLineString', + coordinates: this.steps.map(({ simplifiedGeometry: { coordinates } }) => coordinates), + }; + + this.bounds = this.steps.slice(1).reduce((res, { bounds }) => { + return res.extend(bounds); + }, this.steps[0].bounds); + } + + get strStartDate(): string { + return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format( + this.steps[0].date, ); } + + get stepsItems(): string[] { + const items: string[] = this.isLoop + ? [`${this.from} <> ${this.from}`] + : [`${this.from} > ${this.to}`]; + if (this.steps.length > 0) { + items.push( + this.steps.length > 1 ? `${this.steps.length} étapes` : '1 étape', + `${Math.round( + this.steps.reduce((res, { distance }) => res + distance, 0) / this.steps.length, + )} kms / jour en moyenne`, + ); + } + + return items; + } } diff --git a/src/layout/index.tsx b/src/layout/index.tsx index c8c5907..b8622f0 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -8,8 +8,8 @@ function Layout({ element }: WrapPageElementBrowserArgs): JSX.Element { display="flex" flexDirection="column" sx={{ - minHeight: 'calc(100vh - 4rem)', - '&': { minHeight: 'calc(100dvh - 4rem)' }, + minHeight: '100vh', + '&': { minHeight: '100dvh' }, overflowX: 'hidden', }} > diff --git a/src/pages/components/card.tsx b/src/pages/components/card.tsx index 99a9463..c02a376 100644 --- a/src/pages/components/card.tsx +++ b/src/pages/components/card.tsx @@ -1,27 +1,19 @@ -import { Box, Card, CardBody, Heading, LinkBox, LinkOverlay, Tag, Text } from '@chakra-ui/react'; +import { Box, Card, CardBody, Heading, LinkBox, LinkOverlay, Text } from '@chakra-ui/react'; import { Link } from 'gatsby'; import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'; import React, { Fragment } from 'react'; -import { Trip, tripThemesMap } from '../../fixtures/trips/trip'; +import { Trip } from '../../fixtures'; + +import TripThemes from './themes'; function TripCard({ - trip: { title, steps, themes, isLoop, from, to, description }, + trip: { key, coverImageDescription, title, strStartDate, stepsItems, themes, description }, image, }: { image: IGatsbyImageData | undefined; trip: Trip; }): JSX.Element { - const stepsItems: string[] = isLoop ? [`${from} <> ${from}`] : [`${from} > ${to}`]; - if (steps.length > 0) { - stepsItems.push( - steps.length > 1 ? `${steps.length} étapes` : '1 étape', - `${Math.round( - steps.reduce((res, { distance }) => res + distance, 0) / steps.length, - )} kms / jour en moyenne`, - ); - } - return ( {image && ( - + {title} @@ -53,9 +45,7 @@ function TripCard({ fontSize="0.9rem" sx={{ '&:first-letter': { textTransform: 'uppercase' } }} > - {new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format( - steps[0].date, - )} + {strStartDate} {stepsItems.map((item, index) => ( @@ -66,18 +56,7 @@ function TripCard({ ))} - {themes.length > 0 && ( - - {themes.map((theme) => { - const { color, label } = tripThemesMap[theme]; - return ( - - {label} - - ); - })} - - )} + {description} diff --git a/src/pages/components/list.tsx b/src/pages/components/list.tsx index 97e6870..cf219bb 100644 --- a/src/pages/components/list.tsx +++ b/src/pages/components/list.tsx @@ -2,8 +2,7 @@ import { Box, Button, ButtonGroup, Text } from '@chakra-ui/react'; import { graphql, useStaticQuery } from 'gatsby'; import React, { useEffect, useState } from 'react'; -import { trips } from '../../fixtures/trips'; -import { Trip } from '../../fixtures/trips/trip'; +import { Trip, trips } from '../../fixtures'; import TripCard from './card'; diff --git a/src/pages/components/map.tsx b/src/pages/components/map.tsx index 82b81ae..0334302 100644 --- a/src/pages/components/map.tsx +++ b/src/pages/components/map.tsx @@ -1,5 +1,5 @@ import { Box, Text } from '@chakra-ui/react'; -import { LngLatBounds, Map as MaplibreMap, NavigationControl } from 'maplibre-gl'; +import { Map as MaplibreMap, NavigationControl } from 'maplibre-gl'; import React, { useEffect, useState } from 'react'; import { trips } from '../../fixtures/trips'; @@ -15,15 +15,9 @@ function Map(): JSX.Element { useEffect(() => { if (initialized) { - const positions = trips.flatMap(({ simplifiedGeometry }) => - simplifiedGeometry.coordinates.flatMap(([lng, lat]) => ({ lat, lng })), - ); - const bounds = positions.slice(1).reduce( - (res, position) => { - return res.extend(position); - }, - new LngLatBounds(positions[0], positions[0]), - ); + const bounds = trips.slice(1).reduce((res, { bounds }) => { + return res.extend(bounds); + }, trips[0].bounds); const map = new MaplibreMap({ container: mapId, diff --git a/src/pages/components/themes.tsx b/src/pages/components/themes.tsx new file mode 100644 index 0000000..21ea647 --- /dev/null +++ b/src/pages/components/themes.tsx @@ -0,0 +1,23 @@ +import { Box, Tag } from '@chakra-ui/react'; +import React from 'react'; + +import { TTripTheme, tripThemesMap } from '../../fixtures'; + +function TripThemes({ themes }: { themes: TTripTheme[] }): JSX.Element { + if (themes.length === 0) return <>; + + return ( + + {themes.map((theme) => { + const { color, label } = tripThemesMap[theme]; + return ( + + {label} + + ); + })} + + ); +} + +export default TripThemes; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0d8c449..4e69740 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -21,7 +21,7 @@ function IndexPage({}: PageProps): JSX.Element { @@ -45,8 +45,6 @@ function IndexPage({}: PageProps): JSX.Element { ); } -export default IndexPage; - export const Head: HeadFC = () => { return ( <> @@ -58,3 +56,5 @@ export const Head: HeadFC = () => { ); }; + +export default IndexPage; diff --git a/src/pages/trip/index.tsx b/src/pages/trip/index.tsx new file mode 100644 index 0000000..c18fd5b --- /dev/null +++ b/src/pages/trip/index.tsx @@ -0,0 +1,129 @@ +import { Box, Divider, Heading, Text } from '@chakra-ui/react'; +import { PageProps, graphql } from 'gatsby'; +import { GatsbyImage } from 'gatsby-plugin-image'; +import React, { Fragment, useState } from 'react'; + +import { trips } from '../../fixtures'; +import PageBreadcrumb from '../../layout/breadcrumb'; +import TripThemes from '../components/themes'; + +import TripMap from './map'; +import Step from './step'; + +type TTripPageContext = { tripIndex: number }; + +function TripPage({ + data: { allFile }, + pageContext: { tripIndex }, +}: PageProps): JSX.Element { + const [trip] = useState(trips[tripIndex]); + const [coverImage] = useState( + allFile?.nodes.find(({ relativePath }) => relativePath === trip.coverImage)?.childImageSharp + ?.gatsbyImageData, + ); + + const { + key, + coverImageDescription, + title, + strStartDate, + stepsItems, + themes, + description, + steps, + } = trip; + + return ( + <> + + + + {title} + + + + {coverImage && ( + + )} + + + + {coverImageDescription} + + + + + + Résumé + + + + + {strStartDate} + + + {stepsItems.map((item, index) => ( + + {index > 0 && <> • } + {item} + + ))} + + + + + {description} + + + + + + Itinéraire + + + + + + Étapes + + {steps.map((step, index) => ( + + {index > 0 && } + + + ))} + + + + ); +} + +export function Head({ pageContext: { tripIndex } }: PageProps) { + const trip = trips[tripIndex]; + const { title, description } = trip; + + return ( + <> + {title} | Nos voyages à vélo + + + ); +} + +export const query = graphql` + fragment TripStepFile on File { + relativePath + childImageSharp { + gatsbyImageData(width: 800, placeholder: DOMINANT_COLOR, layout: CONSTRAINED) + } + } + query Trip($queryImages: Boolean!, $imageIds: [String]) { + allFile(filter: { id: { in: $imageIds } }) @include(if: $queryImages) { + nodes { + ...TripStepFile + } + } + } +`; + +export default TripPage; diff --git a/src/pages/trip/map.tsx b/src/pages/trip/map.tsx new file mode 100644 index 0000000..4777292 --- /dev/null +++ b/src/pages/trip/map.tsx @@ -0,0 +1,83 @@ +import { Box, Text } from '@chakra-ui/react'; +import { Map as MaplibreMap, NavigationControl } from 'maplibre-gl'; +import React, { useEffect, useState } from 'react'; + +import { Trip } from '../../fixtures/trips/trip'; + +const mapId = 'trip-map'; + +function TripMap({ trip: { bounds, title, steps } }: { trip: Trip }): JSX.Element { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + setInitialized(true); + }, []); + + useEffect(() => { + if (initialized) { + const map = new MaplibreMap({ + container: mapId, + style: 'https://api.maptiler.com/maps/dataviz/style.json?key=86zpcoLHulCFmgXh2OLu', + bounds, + fitBoundsOptions: { padding: 50 }, + scrollZoom: false, + pitchWithRotate: false, + }); + + map.addControl(new NavigationControl({ showZoom: true })); + + map.on('load', () => { + map.addSource('trips', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: steps.map(({ simplifiedGeometry }) => ({ + type: 'Feature', + geometry: simplifiedGeometry, + properties: {}, + })), + }, + }); + + map.addLayer( + { + id: 'trips', + type: 'line', + source: 'trips', + paint: { + 'line-color': ['get', 'color'], + 'line-width': 5, + }, + }, + 'Ocean labels', + ); + + map.addLayer( + { + id: 'trips-border', + type: 'line', + source: 'trips', + paint: { + 'line-color': '#fff', + 'line-width': 9, + }, + }, + 'trips', + ); + }); + } + }, [initialized]); + + return ( + + + + + Itinéraire de l'étape {title} + + + + ); +} + +export default TripMap; diff --git a/src/pages/trip/step.tsx b/src/pages/trip/step.tsx new file mode 100644 index 0000000..3fcc42a --- /dev/null +++ b/src/pages/trip/step.tsx @@ -0,0 +1,74 @@ +import { Avatar, Box, Heading, Text } from '@chakra-ui/react'; +import { GatsbyImage, IGatsbyImageData } from 'gatsby-plugin-image'; +import React, { useState } from 'react'; + +import { TripStep } from '../../fixtures/trips/trip'; + +function Step({ + stepIndex, + step: { title, distance, photos }, + files, +}: { + files: readonly Queries.TripStepFileFragment[]; + step: TripStep; + stepIndex: number; +}): JSX.Element { + const [images] = useState( + photos.reduce>( + (res, { path, description }) => { + const file = files.find(({ relativePath }) => relativePath === path); + if (file?.childImageSharp?.gatsbyImageData) { + res.push({ image: file.childImageSharp.gatsbyImageData, description }); + } + + return res; + }, + [], + ), + ); + + return ( + + + + + + {title} + + + + + {distance} kms + + + + {images.length > 0 && ( + + + Photos + + + {images.map(({ image, description }, index) => ( + + + + ))} + + + )} + + ); +} + +export default Step; diff --git a/src/theme.ts b/src/theme.ts index 19b2d5f..b02dd8a 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,11 +1,13 @@ import { theme as chakraTheme, extendBaseTheme } from '@chakra-ui/react'; -const { Button, Card, Tag } = chakraTheme.components; +const { Avatar, Button, Card, Divider, Tag } = chakraTheme.components; const theme = extendBaseTheme({ components: { + Avatar, Button, Card, + Divider, Tag, }, });