From 2227bd8da12a57263fc3f72af0058670a53a84f6 Mon Sep 17 00:00:00 2001 From: Lucieo Date: Wed, 7 Feb 2024 16:17:48 +0000 Subject: [PATCH] Disponibilities public listing (#48) --- .../1.0.0/full_documentation.json | 2 +- web/components/Campaign/CampaignContext.tsx | 2 + web/components/Campaign/CampaignHelper.tsx | 35 +++ web/components/Campaign/CampaignProvider.tsx | 6 +- .../HomeInsert/HomeCampaignInsert.tsx | 5 +- web/components/Home/HomePlaces.tsx | 55 ++-- web/components/Home/HomeSearch.tsx | 5 +- web/components/MobileMenu.tsx | 22 +- web/components/Navigation/Header.tsx | 13 +- web/components/Place/PlaceGridCard.tsx | 22 ++ web/components/Place/PlaceSearch.tsx | 17 +- web/components/Place/PlacesPage.tsx | 245 ++++++++++++++++++ web/components/Place/PlacesTab.tsx | 114 ++++++++ web/components/Tag.tsx | 14 + web/hooks/useInfinitePlaces.tsx | 9 +- web/hooks/usePlaces.ts | 2 +- web/pages/espaces/index.tsx | 210 +-------------- web/public/locales/fr/common.json | 13 +- web/public/locales/fr/home.json | 13 +- web/theme/components/Tag.ts | 29 --- 20 files changed, 541 insertions(+), 292 deletions(-) create mode 100644 web/components/Campaign/CampaignHelper.tsx create mode 100644 web/components/Place/PlacesPage.tsx create mode 100644 web/components/Place/PlacesTab.tsx diff --git a/back/extensions/documentation/documentation/1.0.0/full_documentation.json b/back/extensions/documentation/documentation/1.0.0/full_documentation.json index 137ce3df..8c864e5e 100644 --- a/back/extensions/documentation/documentation/1.0.0/full_documentation.json +++ b/back/extensions/documentation/documentation/1.0.0/full_documentation.json @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "02/06/2024 10:37:22 AM" + "x-generation-date": "02/07/2024 10:13:36 AM" }, "x-strapi-config": { "path": "/documentation", diff --git a/web/components/Campaign/CampaignContext.tsx b/web/components/Campaign/CampaignContext.tsx index ab124b40..274879d0 100644 --- a/web/components/Campaign/CampaignContext.tsx +++ b/web/components/Campaign/CampaignContext.tsx @@ -15,10 +15,12 @@ export type ActiveCampaign = Campaign & { interface ICampaignContext { activeCampaigns?: ActiveCampaign[] currentCampaign?: ActiveCampaign + isLoading?: boolean } const CampaignContext = React.createContext({ activeCampaigns: undefined, + isLoading: false, }) export default CampaignContext diff --git a/web/components/Campaign/CampaignHelper.tsx b/web/components/Campaign/CampaignHelper.tsx new file mode 100644 index 00000000..d64ea309 --- /dev/null +++ b/web/components/Campaign/CampaignHelper.tsx @@ -0,0 +1,35 @@ +import { HStack, Text, Flex, Button, StackProps } from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' +import { ActiveCampaign } from '~components/Campaign/CampaignContext' + +const CampaignHelper = ({ + campaign, + ...props +}: { campaign?: ActiveCampaign } & StackProps) => { + const { t } = useTranslation('common') + + return ( + + + + {campaign ? campaign?.title : t('solidarity.helper_title')} + + {campaign ? campaign.description : t('solidarity.helper')} + + + + + + ) +} + +export default CampaignHelper diff --git a/web/components/Campaign/CampaignProvider.tsx b/web/components/Campaign/CampaignProvider.tsx index 9545208c..163c747e 100644 --- a/web/components/Campaign/CampaignProvider.tsx +++ b/web/components/Campaign/CampaignProvider.tsx @@ -17,12 +17,15 @@ const CampaignProvider = ({ children }: ICampaignProvider) => { }), [], ) - const { data: campaigns } = useCampaigns(activeCampaignsQueryParameters) + const { data: campaigns, isLoading } = useCampaigns( + activeCampaignsQueryParameters, + ) useEffect(() => { if (Boolean(campaigns?.length)) { const activeCampaigns = campaigns?.map((campaign) => { const mode = getCampaignMode(campaign) + // const mode = 'applications' const limitDate = getLimitDate(campaign, mode) return { ...campaign, mode, limitDate } }) @@ -72,6 +75,7 @@ const CampaignProvider = ({ children }: ICampaignProvider) => { value={{ activeCampaigns, currentCampaign: activeCampaigns?.[0], + isLoading, }} > {children} diff --git a/web/components/Campaign/HomeInsert/HomeCampaignInsert.tsx b/web/components/Campaign/HomeInsert/HomeCampaignInsert.tsx index 3e1a3c34..9fc866e6 100644 --- a/web/components/Campaign/HomeInsert/HomeCampaignInsert.tsx +++ b/web/components/Campaign/HomeInsert/HomeCampaignInsert.tsx @@ -1,9 +1,10 @@ -import { VStack, Text, Tag, Box, Button, Stack } from '@chakra-ui/react' +import { VStack, Text, Box, Button, Stack } from '@chakra-ui/react' import Hands from 'public/assets/img/hands-outline.svg' import theme from '~theme' import { useTranslation } from 'next-i18next' import { ROUTE_ACCOUNT_PLACES, ROUTE_PLACES } from '~constants' import { CampaignMode } from '~components/Campaign/CampaignContext' +import Tag from '~components/Tag' const HomeCampaignInsert = ({ mode, @@ -28,7 +29,7 @@ const HomeCampaignInsert = ({ height="100%" > - {t('campaign.tag', { title })} + {title} { lg: true, }) const { t } = useTranslation('home') - const { data: places } = usePlaces({ - published_eq: true, - _limit: 20, - _sort: 'dispoAsc', - }) + const { t: tCommon } = useTranslation('common') + const filters = campaign + ? { + 'disponibilities.campaign': campaign?.id, + } + : {} + const { data: places } = usePlaces( + { + published_eq: true, + _limit: 20, + _sort: 'dispoAsc', + ...filters, + }, + campaign && 'campaignPlaces', + ) if (!places || places.length === 0) return null @@ -44,38 +55,18 @@ const HomePlaces = ({ campaign }: Props) => { - {campaign - ? t('places.campaign.title', { title: campaign?.title }) - : t('places.title')} + {campaign ? campaign?.title : t('places.title')} {campaign && ( - - {t('places.campaign.tag', { + + {tCommon('campaign.open', { date: format(new Date(campaign?.limitDate), 'd MMMM yyyy'), })} )} - - - - {campaign ? campaign?.title : t('places.solidarity.helper_title')} - - {campaign ? campaign.description : t('places.solidarity.helper')} - - - - - + @@ -89,7 +80,9 @@ const HomePlaces = ({ campaign }: Props) => { color={'campaign.dark'} _hover={{ bg: 'campaign.light', textDecor: 'none' }} > - {t('places.campaign.cta', { title: campaign?.title })} + {t('places.campaign.cta', { + title: campaign?.title.toLowerCase(), + })} ) : ( + + + + {t('search.filterBy.label')} + + + + + )} + + {isGridView ? ( + + ) : ( + + )} + + )} + + + ) +} + +export default PlacesPage diff --git a/web/components/Place/PlacesTab.tsx b/web/components/Place/PlacesTab.tsx new file mode 100644 index 00000000..ecda1cb5 --- /dev/null +++ b/web/components/Place/PlacesTab.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react' +import { + Tabs, + TabPanels, + TabPanel, + TabList, + Tab, + HStack, + Text, +} from '@chakra-ui/react' +import { format } from '~utils/date' +import Tag from '~components/Tag' +import { useTranslation } from 'next-i18next' +import PlacesPage from '~components/Place/PlacesPage' +import useCampaignContext from '~components/Campaign/useCampaignContext' +import { useRouter } from 'next/router' + +const PlacesTabs = () => { + const router = useRouter() + const { currentCampaign } = useCampaignContext() + const { t } = useTranslation('common') + const [selectedTab, setSelectedTab] = useState() + + useEffect(() => { + if (router.query.tab) { + setSelectedTab(parseInt(router.query.tab as string, 10) || 0) + } + }, [router.query.tab]) + + return ( + { + setSelectedTab(index) + router.push({ + pathname: router.pathname, + query: { ...router.query, tab: index }, + }) + }} + variant="enclosed" + index={selectedTab} + > + + + + {t('solidarity.title')} + + + + + + {currentCampaign?.title} + + + + {t('campaign.open', { + date: format( + new Date(currentCampaign?.limitDate), + 'd MMMM yyyy', + ), + })} + + + + + + + + + + + + + + + ) +} + +export default PlacesTabs diff --git a/web/components/Tag.tsx b/web/components/Tag.tsx index c1ad3400..bcf6e901 100644 --- a/web/components/Tag.tsx +++ b/web/components/Tag.tsx @@ -16,6 +16,8 @@ interface Props extends TagProps { | 'occupied' | 'nextweek' | 'expired' + | 'campaign' + | 'solidarity' children?: React.ReactNode } @@ -76,6 +78,18 @@ const Tag = ({ status, children, ...rest }: Props) => { {children || t('tag.occupied')} ) + case 'campaign': + return ( + + {children || t('tag.campaign')} + + ) + case 'solidarity': + return ( + + {children || t('tag.campaign')} + + ) } } diff --git a/web/hooks/useInfinitePlaces.tsx b/web/hooks/useInfinitePlaces.tsx index f59a29ca..aa86f67a 100644 --- a/web/hooks/useInfinitePlaces.tsx +++ b/web/hooks/useInfinitePlaces.tsx @@ -2,9 +2,13 @@ import { useInfiniteQuery } from 'react-query' import { client } from '~api/client-api' import { Espaces } from '~typings/api' -export const useInfinitePlaces = (params: Espaces.EspacesList.RequestQuery) => { +export const useInfinitePlaces = ( + params: Espaces.EspacesList.RequestQuery, + name = 'places', + enabled = true, +) => { return useInfiniteQuery( - ['places', params], + [name, params], ({ pageParam = 0 }) => { return client.espaces .espacesList({ @@ -17,6 +21,7 @@ export const useInfinitePlaces = (params: Espaces.EspacesList.RequestQuery) => { getNextPageParam: (lastPage, pages) => { if (lastPage.length >= params._limit) return pages.length }, + enabled: enabled, }, ) } diff --git a/web/hooks/usePlaces.ts b/web/hooks/usePlaces.ts index 534b8d09..e6167443 100644 --- a/web/hooks/usePlaces.ts +++ b/web/hooks/usePlaces.ts @@ -1,7 +1,7 @@ import { useQuery } from 'react-query' import { client } from '~api/client-api' -export const usePlaces = (query: Record = {}) => { +export const usePlaces = (query: Record = {}, name = 'places') => { return useQuery(['places', query], () => client.espaces.espacesList(query).then((res) => res.data), ) diff --git a/web/pages/espaces/index.tsx b/web/pages/espaces/index.tsx index dd56785d..4a72a763 100644 --- a/web/pages/espaces/index.tsx +++ b/web/pages/espaces/index.tsx @@ -1,213 +1,19 @@ -import React, { useRef, useState, useMemo, useEffect } from 'react' +import React from 'react' import { SSRConfig } from 'next-i18next' import { GetServerSideProps } from 'next' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -import { - Container, - Flex, - Select, - Button, - Text, - ButtonGroup, - useBreakpointValue, -} from '@chakra-ui/react' -import PlaceSearch from '~components/Place/PlaceSearch' -import { useInfinitePlaces } from '~hooks/useInfinitePlaces' -import { useScrollBottom } from '~hooks/useScrollBottom' -import PlaceGrid from '~components/Place/PlaceGrid' -import PlaceList from '~components/Place/PlaceList' -import { formatSearch } from '~utils/search' -import Arrow from 'public/assets/img/arrow-bottom.svg' -import { useTranslation } from 'next-i18next' -import { FormProvider, useForm } from 'react-hook-form' -import { SortOptions } from '~utils/search' -import { useQueryClient } from 'react-query' -import NoResult from '~components/Place/NoResult' -import MobileMap from '~components/Place/MobileMap' -import { useRouter } from 'next/router' -import { NextSeo } from 'next-seo' -import { formatSearchToQuery } from '~utils/search' -import { ROUTE_PLACES } from '~constants' -const styleSelected = { - color: 'blue.500', - borderBottomColor: 'blue.500', -} - -const Places = () => { - const router = useRouter() - const isMobile = useBreakpointValue({ base: true, md: false }) - const { t } = useTranslation('place') - const ref = useRef(null) - const [isGridView, setGridView] = useState(true) - const form = useForm({ - defaultValues: { - perimeter: 15, - ...router.query, - }, - }) - - const [searchParams, setSearchParams] = useState({}) - - useEffect(() => { - setSearchParams(formatSearch(router.query)) - }, []) - const queryClient = useQueryClient() +import useCampaignContext from '~components/Campaign/useCampaignContext' +import PlacesPage from '~components/Place/PlacesPage' - const { - data: places, - isLoading, - fetchNextPage, - hasNextPage, - isFetching, - } = useInfinitePlaces({ - _limit: isGridView ? 12 : 6, - ...searchParams, - }) - const nbPlaces = useMemo(() => places?.pages?.flat().length, [places?.pages]) - const isPlural = useMemo(() => nbPlaces > 1, [nbPlaces]) +import PlacesTabs from '~components/Place/PlacesTab' - useEffect(() => { - if (isMobile) setGridView(true) - }, [isMobile]) - - useScrollBottom( - ref, - () => { - if (hasNextPage && !isFetching) { - fetchNextPage() - } - }, - isGridView, - ) - - const onSubmit = (data) => { - let forceSort - if ( - !searchParams['disponibilities.start_gte'] && - Boolean(data?.startDate) - ) { - forceSort = true - form.setValue('sortBy', 'nbDispoDesc') - } - const newSearch = formatSearch(data, forceSort) - - setSearchParams(newSearch) +const Places = () => { + const { currentCampaign } = useCampaignContext() - router.push({ - pathname: ROUTE_PLACES, - query: formatSearchToQuery(data), - }) - } + if (!currentCampaign) return - return ( - - - - - {!isLoading && !isFetching && places?.pages?.flat().length === 0 ? ( - - ) : ( - <> - - - - {nbPlaces > 0 && ( - <> - - - {t( - `search.${ - Object.keys(router.query).filter( - (k) => k !== 'sortBy', - ).length > 0 - ? 'nbPlacesWithDispo' - : 'nbPlace' - }${isPlural ? 's' : ''}`, - { - nb: nbPlaces, - }, - )} - - - )} - - {!isMobile && ( - - - - - - {t('search.filterBy.label')} - - - - - )} - - {isGridView ? ( - - ) : ( - - )} - - )} - - - ) + return } export const getServerSideProps: GetServerSideProps = async ({ diff --git a/web/public/locales/fr/common.json b/web/public/locales/fr/common.json index da2bbf8e..6aef3d71 100644 --- a/web/public/locales/fr/common.json +++ b/web/public/locales/fr/common.json @@ -84,5 +84,16 @@ "error": "Impossible de confirmer l'email", "success": "Merci ! Votre mail est validé et votre compte est maintenant en attente d’activation par StudioD. Vous recevrez une notification par mail dès qu’elle sera effective. Vous pouvez, en attendant, modifier vos informations.", "btn": "Retour à l’accueil" - } + }, + "solidarity": { + "title": "Créneaux solidaires", + "tag": "Créneaux solidaires", + "helper_title": "Les créneaux solidaires ", + "helper": " sont proposés par les lieux inscrits sur la plateforme et sont réservables toute l'année selon les disponibilités proposées. La réservation est ouverte à toute compagnie inscrite sur Studio D" + }, + "campaign": { + "open": "Ouvert jusqu'au {{date}}", + "partner": "Partenaire {{title}}" + }, + "show": "Découvrir" } diff --git a/web/public/locales/fr/home.json b/web/public/locales/fr/home.json index 00ec0384..70075dcb 100644 --- a/web/public/locales/fr/home.json +++ b/web/public/locales/fr/home.json @@ -14,16 +14,10 @@ "places": { "title": "Prochains créneaux disponibles", "btn": "Parcourir tous les espaces", - "solidarity": { - "helper_title": "Les créneaux solidaires ", - "helper": " sont proposés par les lieux inscrits sur la plateforme et sont réservables toute l'année selon les disponibilités proposées. La réservation est ouverte à toute compagnie inscrite sur Studio D" - }, "campaign": { - "tag": "Ouvert jusqu'au {{date}}", "title": "Dispositif {{title}} ", - "cta": "Parcourir tous les espaces partenaires du dispositif {{title}}" - }, - "show": "Découvrir" + "cta": "Parcourir tous les espaces partenaires {{title}}" + } }, "news": { "title": "Les actus de la communauté", @@ -43,8 +37,5 @@ "cta": "Parcourir" } } - }, - "solidarity": { - "tag": "Créneaux solidaires" } } diff --git a/web/theme/components/Tag.ts b/web/theme/components/Tag.ts index dde72e43..dde72210 100644 --- a/web/theme/components/Tag.ts +++ b/web/theme/components/Tag.ts @@ -1,18 +1,3 @@ -import theme from '~theme' -import { campaign } from '~theme/colors' - -const baseContainerTag = { - container: { - fontSize: 'sm', - px: 2, - py: 1, - whiteSpace: 'pre', - lineHeight: 1, - borderRadius: 'sm', - color: 'white', - }, -} - const Tag = { variants: { subtle: { @@ -25,20 +10,6 @@ const Tag = { lineHeight: 1, }, }, - campaign: { - container: { - ...baseContainerTag, - backgroundColor: campaign.light, - color: campaign.dark, - }, - }, - blue: { - container: { - ...baseContainerTag, - backgroundColor: 'blue.200', - color: 'blue.500', - }, - }, }, sizes: { md: {