diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index cacd5ed91..305e5dccb 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -84,12 +84,6 @@ jobs: NEXT_PUBLIC_GHOST_URL_IMAGES_SOURCE=${{ vars.NEXT_PUBLIC_GHOST_URL_IMAGES_SOURCE }} NEXT_PUBLIC_GHOST_KEY=${{ vars.NEXT_PUBLIC_GHOST_KEY }} NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL=${{ vars.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL }} - NEXT_PUBLIC_BREVO_API_URL=${{ vars.NEXT_PUBLIC_BREVO_API_URL }} - BREVO_API_KEY=${{ secrets.BREVO_API_KEY }} - S3_CONFIG_ACCESS_KEY_ID=${{ secrets.S3_CONFIG_ACCESS_KEY_ID }} - S3_CONFIG_SECRET_ACCESS_KEY=${{ secrets.S3_CONFIG_SECRET_ACCESS_KEY }} - S3_CONFIG_REGION=${{ secrets.S3_CONFIG_REGION }} - S3_CONFIG_ENDPOINT=${{ secrets.S3_CONFIG_ENDPOINT }} NEXT_PUBLIC_MATOMO_URL=${{ vars.NEXT_PUBLIC_MATOMO_URL }} NEXT_PUBLIC_MATOMO_SITE_ID=${{ vars.NEXT_PUBLIC_MATOMO_SITE_ID }} diff --git a/dockerfile b/dockerfile index d704f0c73..b16390b2a 100644 --- a/dockerfile +++ b/dockerfile @@ -24,16 +24,9 @@ ARG NEXT_PUBLIC_BAL_API_URL ARG NEXT_PUBLIC_GHOST_URL ARG NEXT_PUBLIC_GHOST_URL_IMAGES_SOURCE ARG NEXT_PUBLIC_GHOST_KEY -ARG S3_CONFIG_ACCESS_KEY_ID -ARG S3_CONFIG_SECRET_ACCESS_KEY -ARG S3_CONFIG_REGION -ARG S3_CONFIG_ENDPOINT ARG NEXT_PUBLIC_MATOMO_URL ARG NEXT_PUBLIC_MATOMO_SITE_ID -ARG NEXT_PUBLIC_BREVO_API_URL -ARG BREVO_API_KEY ARG NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL -ARG NEXT_PUBLIC_ALTCHA_API_KEY # Les variables d'environnement sont exportées avant le build ENV NEXT_PUBLIC_API_BAN_URL=${NEXT_PUBLIC_API_BAN_URL} @@ -47,16 +40,10 @@ ENV NEXT_PUBLIC_BAL_API_URL=${NEXT_PUBLIC_BAL_API_URL} ENV NEXT_PUBLIC_GHOST_URL=${NEXT_PUBLIC_GHOST_URL} ENV NEXT_PUBLIC_GHOST_URL_IMAGES_SOURCE=${NEXT_PUBLIC_GHOST_URL_IMAGES_SOURCE} ENV NEXT_PUBLIC_GHOST_KEY=${NEXT_PUBLIC_GHOST_KEY} -ENV S3_CONFIG_ACCESS_KEY_ID=${S3_CONFIG_ACCESS_KEY_ID} -ENV S3_CONFIG_SECRET_ACCESS_KEY=${S3_CONFIG_SECRET_ACCESS_KEY} -ENV S3_CONFIG_REGION=${S3_CONFIG_REGION} -ENV S3_CONFIG_ENDPOINT=${S3_CONFIG_ENDPOINT} ENV NEXT_PUBLIC_MATOMO_URL=${NEXT_PUBLIC_MATOMO_URL} ENV NEXT_PUBLIC_MATOMO_SITE_ID=${NEXT_PUBLIC_MATOMO_SITE_ID} -ENV NEXT_PUBLIC_BREVO_API_URL=${NEXT_PUBLIC_BREVO_API_URL} ENV NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL=${NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL} -ENV BREVO_API_KEY=${BREVO_API_KEY} -ENV NEXT_PUBLIC_ALTCHA_API_KEY=${NEXT_PUBLIC_ALTCHA_API_KEY} + RUN npm run build diff --git a/package-lock.json b/package-lock.json index 31b0e8b1f..af76bdd46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "lodash": "^4.17.21", "maplibre-gl": "^4.7.1", "next": "14.2.5", + "next-runtime-env": "^3.2.2", "node-fetch": "^3.3.2", "path": "^0.12.7", "prop-types": "^15.8.1", @@ -13745,6 +13746,19 @@ } } }, + "node_modules/next-runtime-env": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/next-runtime-env/-/next-runtime-env-3.2.2.tgz", + "integrity": "sha512-S5S6NxIf3XeaVc9fLBN2L5Jzu+6dLYCXeOaPQa1RzKRYlG2BBayxXOj6A4VsciocyNkJMazW1VAibtbb1/ZjAw==", + "dependencies": { + "next": "^14", + "react": "^18" + }, + "peerDependencies": { + "next": "^14", + "react": "^18" + } + }, "node_modules/no-case": { "version": "3.0.4", "dev": true, diff --git a/package.json b/package.json index bc87ef11e..8d6022928 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "lodash": "^4.17.21", "maplibre-gl": "^4.7.1", "next": "14.2.5", + "next-runtime-env": "^3.2.2", "node-fetch": "^3.3.2", "path": "^0.12.7", "prop-types": "^15.8.1", diff --git a/public/documentation-bal/book.png b/public/documentation-bal/book.png new file mode 100644 index 000000000..2094ca987 Binary files /dev/null and b/public/documentation-bal/book.png differ diff --git a/public/documentation-bal/community.png b/public/documentation-bal/community.png new file mode 100644 index 000000000..249bf2e4c Binary files /dev/null and b/public/documentation-bal/community.png differ diff --git a/public/documentation-bal/document-download.png b/public/documentation-bal/document-download.png new file mode 100644 index 000000000..9ff4b41b1 Binary files /dev/null and b/public/documentation-bal/document-download.png differ diff --git a/public/documentation-bal/guide-bonnes-pratiques-v4.1.pdf b/public/documentation-bal/guide-bonnes-pratiques-v4.1.pdf new file mode 100644 index 000000000..f641f0c05 Binary files /dev/null and b/public/documentation-bal/guide-bonnes-pratiques-v4.1.pdf differ diff --git a/public/documentation-bal/guide-bonnes-pratiques.png b/public/documentation-bal/guide-bonnes-pratiques.png new file mode 100644 index 000000000..6fec77b31 Binary files /dev/null and b/public/documentation-bal/guide-bonnes-pratiques.png differ diff --git a/public/documentation-bal/guide-mes-adresses-v8.2.pdf b/public/documentation-bal/guide-mes-adresses-v8.2.pdf new file mode 100644 index 000000000..fd38e0020 Binary files /dev/null and b/public/documentation-bal/guide-mes-adresses-v8.2.pdf differ diff --git a/public/documentation-bal/guide-mes-adresses.png b/public/documentation-bal/guide-mes-adresses.png new file mode 100644 index 000000000..12acdd8e3 Binary files /dev/null and b/public/documentation-bal/guide-mes-adresses.png differ diff --git a/public/documentation-bal/logo-bal.png b/public/documentation-bal/logo-bal.png new file mode 100644 index 000000000..604f8882f Binary files /dev/null and b/public/documentation-bal/logo-bal.png differ diff --git a/public/documentation-bal/logo-ban.png b/public/documentation-bal/logo-ban.png new file mode 100644 index 000000000..2336aaab9 Binary files /dev/null and b/public/documentation-bal/logo-ban.png differ diff --git a/public/documentation-bal/questions.png b/public/documentation-bal/questions.png new file mode 100644 index 000000000..29240e436 Binary files /dev/null and b/public/documentation-bal/questions.png differ diff --git a/public/documentation-bal/ressources.png b/public/documentation-bal/ressources.png new file mode 100644 index 000000000..49f4a0229 Binary files /dev/null and b/public/documentation-bal/ressources.png differ diff --git a/src/app/api/brevo-newsletters/route.ts b/src/app/api/brevo-newsletters/route.ts index e3ba8406b..fb4ad8295 100644 --- a/src/app/api/brevo-newsletters/route.ts +++ b/src/app/api/brevo-newsletters/route.ts @@ -1,21 +1,26 @@ import { customFetch } from '@/lib/fetch' import { NextRequest, NextResponse } from 'next/server' +import { env } from 'next-runtime-env' -if (!process.env.BREVO_API_KEY || !process.env.NEXT_PUBLIC_BREVO_API_URL) { - throw new Error('BREVO_API_KEY or NEXT_PUBLIC_BREVO_API_URL is not defined in the environment') -} +export const dynamic = 'force-dynamic' // Optin to the newsletter export async function POST(request: NextRequest) { + if (!env('BREVO_API_KEY') || !env('NEXT_PUBLIC_BREVO_API_URL')) { + return NextResponse.json( + { error: 'BREVO_API_KEY or NEXT_PUBLIC_BREVO_API_URL is not defined in the environment' }, + { status: 500 } + ) + } const requestBody = await request.json() const { email } = requestBody const options = { method: 'POST', - headers: { 'accept': 'application/json', 'content-type': 'application/json', 'api-key': process.env.BREVO_API_KEY } as RequestInit['headers'], + headers: { 'accept': 'application/json', 'content-type': 'application/json', 'api-key': env('BREVO_API_KEY') } as RequestInit['headers'], body: JSON.stringify({ email, listIds: [10] }), } - const response = await customFetch(`${process.env.NEXT_PUBLIC_BREVO_API_URL}/contacts`, options) + const response = await customFetch(`${env('NEXT_PUBLIC_BREVO_API_URL')}/contacts`, options) return NextResponse.json(response) } @@ -30,14 +35,14 @@ export async function GET() { try { do { const { campaigns, count: _count } = await customFetch( - `${process.env.NEXT_PUBLIC_BREVO_API_URL}/emailCampaigns?status=sent&limit=30&offset=${offset}&startDate=${from.toISOString()}&endDate=${new Date( + `${env('NEXT_PUBLIC_BREVO_API_URL')}/emailCampaigns?status=sent&limit=30&offset=${offset}&startDate=${from.toISOString()}&endDate=${new Date( to ).toISOString()}`, { headers: { 'accept': 'application/json', 'content-type': 'application/json', - 'api-key': process.env.BREVO_API_KEY, + 'api-key': env('BREVO_API_KEY'), } as RequestInit['headers'], } ) diff --git a/src/app/api/certificat/[idAdresse]/components/certificat.tsx b/src/app/api/certificat/[idAdresse]/components/certificat.tsx index 86343ec39..7c7b8fe1a 100644 --- a/src/app/api/certificat/[idAdresse]/components/certificat.tsx +++ b/src/app/api/certificat/[idAdresse]/components/certificat.tsx @@ -8,8 +8,9 @@ import { View, } from '@react-pdf/renderer' import { stylesDSFR } from './certificat.stylesheet' +import { env } from 'next-runtime-env' -const NEXT_PUBLIC_ADRESSE_URL = process.env.NEXT_PUBLIC_ADRESSE_URL +const NEXT_PUBLIC_ADRESSE_URL = env('NEXT_PUBLIC_ADRESSE_URL') interface CertificatNumerotationProps { data: { @@ -29,9 +30,10 @@ interface CertificatNumerotationProps { telephone?: string email?: string } + logoUrl: string } -const CertificatNumerotation: React.FC = ({ data, qrCodeDataURL, mairie }) => { +const CertificatNumerotation: React.FC = ({ data, qrCodeDataURL, mairie, logoUrl }) => { const nomCommune = data.full_address.districtDefaultLabel const libelleVoie = data.full_address.commonToponymDefaultLabel const numero = data.full_address.number @@ -39,7 +41,7 @@ const CertificatNumerotation: React.FC = ({ data, q const { cog } = data.full_address const parcelles = data.cadastre_ids.map(id => id.replace(/(\d+)([A-Z])/, '$1 $2')) - const logoUrl = `public/logos/certificat/${cog}.png` + // const logoUrl = `public/logos/certificat/${cog}.png` const logoAdresse = `public/logos/certificat/adresse-logo.png` const dateObj = new Date(data.createdAt) @@ -66,6 +68,7 @@ const CertificatNumerotation: React.FC = ({ data, q + {'\n'} {/* Conteneur pour le logo de la mairie et les informations */} diff --git a/src/app/api/certificat/[idAdresse]/route.tsx b/src/app/api/certificat/[idAdresse]/route.tsx index adbeefbb0..e6944f5a0 100644 --- a/src/app/api/certificat/[idAdresse]/route.tsx +++ b/src/app/api/certificat/[idAdresse]/route.tsx @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' import { getAddress, getDistrict } from '@/lib/api-ban' -const NEXT_PUBLIC_API_BAN_URL = process.env.NEXT_PUBLIC_API_BAN_URL -const BAN_API_TOKEN = process.env.BAN_API_TOKEN +import { env } from 'next-runtime-env' + +const NEXT_PUBLIC_API_BAN_URL = env('NEXT_PUBLIC_API_BAN_URL') +const BAN_API_TOKEN = env('BAN_API_TOKEN') const isAddressCertifiable = async ({ banId, sources, certifie, parcelles }: any): Promise => { return !!banId && sources?.includes('bal') && certifie && parcelles?.length > 0 @@ -11,10 +13,8 @@ const isDistrictCertifiable = async (banIdDistrict: string | null): Promise ) diff --git a/src/app/api/proxy-flag-commune/[codeCommune]/route.ts b/src/app/api/proxy-flag-commune/[codeCommune]/route.ts new file mode 100644 index 000000000..b79d610e6 --- /dev/null +++ b/src/app/api/proxy-flag-commune/[codeCommune]/route.ts @@ -0,0 +1,9 @@ +import { getCommuneFlag } from '@/lib/api-blasons-communes' +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest, { params }: { params: { codeCommune: string } }) { + const { codeCommune } = params + const flagUrl = await getCommuneFlag(codeCommune) + + return NextResponse.json(flagUrl) +} diff --git a/src/app/blog/[slug]/components/AsideContent.tsx b/src/app/blog/[slug]/components/AsideContent.tsx index 3219fb714..ea9773e96 100644 --- a/src/app/blog/[slug]/components/AsideContent.tsx +++ b/src/app/blog/[slug]/components/AsideContent.tsx @@ -3,13 +3,14 @@ import Link from 'next/link' import { getPosts } from '@/lib/blog' import { AsideLinkList, AsideFollowList } from './AsideContent.styled' +import { env } from 'next-runtime-env' + +const SOCIAL_NETWORKS_URL_MASTODON = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_MASTODON') +const SOCIAL_NETWORKS_URL_FACEBOOK = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_FACEBOOK') +const SOCIAL_NETWORKS_URL_LINKEDIN = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_LINKEDIN') +const SOCIAL_NETWORKS_URL_GITHUB = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_GITHUB') +const SOCIAL_NETWORKS_URL_XCOM = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_XCOM') -const { - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_XCOM: SOCIAL_NETWORKS_URL_XCOM, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_MASTODON: SOCIAL_NETWORKS_URL_MASTODON, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_FACEBOOK: SOCIAL_NETWORKS_URL_FACEBOOK, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_LINKEDIN: SOCIAL_NETWORKS_URL_LINKEDIN, -} = process.env const NB_HIGHLIGHTED_POSTS = 3 async function AsideContent() { diff --git a/src/app/carte-base-adresse-nationale/components/ban-map/LayerBan/LayerBan.tsx b/src/app/carte-base-adresse-nationale/components/ban-map/LayerBan/LayerBan.tsx index ae12d7ea6..7b06e8b5a 100644 --- a/src/app/carte-base-adresse-nationale/components/ban-map/LayerBan/LayerBan.tsx +++ b/src/app/carte-base-adresse-nationale/components/ban-map/LayerBan/LayerBan.tsx @@ -11,8 +11,9 @@ import { import type { FillLayer } from 'react-map-gl/maplibre' import type { Address } from '../types.d' +import { env } from 'next-runtime-env' -const API_BAN_URL = process.env.NEXT_PUBLIC_API_BAN_URL +const API_BAN_URL = env('NEXT_PUBLIC_API_BAN_URL') function LayerBan({ address }: { address: Address }) { const isAdresseFilter = useMemo(() => ['==', ['get', 'id'], address?.id], [address]) diff --git a/src/app/carte-base-adresse-nationale/components/district/DistrictMicroToponymList.tsx b/src/app/carte-base-adresse-nationale/components/district/DistrictMicroToponymList.tsx index a18598cd2..c79e25397 100644 --- a/src/app/carte-base-adresse-nationale/components/district/DistrictMicroToponymList.tsx +++ b/src/app/carte-base-adresse-nationale/components/district/DistrictMicroToponymList.tsx @@ -6,8 +6,9 @@ import { Table } from '@codegouvfr/react-dsfr/Table' import { DistrictMicroToponymListInfo } from './DistrictMicroToponymList.styles' import type { TypeDistrictExtended } from '../../types/LegacyBan.types' +import { env } from 'next-runtime-env' -const URL_CARTOGRAPHY_BAN = process.env.NEXT_PUBLIC_URL_CARTOGRAPHY_BAN +const URL_CARTOGRAPHY_BAN = env('NEXT_PUBLIC_URL_CARTOGRAPHY_BAN') interface DistrictMicroToponymListProps { district: TypeDistrictExtended diff --git a/src/app/carte-base-adresse-nationale/components/micro-toponym/MicroToponymAddressList.tsx b/src/app/carte-base-adresse-nationale/components/micro-toponym/MicroToponymAddressList.tsx index 4b02384e2..264b60895 100644 --- a/src/app/carte-base-adresse-nationale/components/micro-toponym/MicroToponymAddressList.tsx +++ b/src/app/carte-base-adresse-nationale/components/micro-toponym/MicroToponymAddressList.tsx @@ -11,8 +11,9 @@ import { import type { SortAddressesEntry } from '../../tools/sortAddresses' import type { TypeMicroToponymExtended } from '../../types/LegacyBan.types' +import { env } from 'next-runtime-env' -const URL_CARTOGRAPHY_BAN = process.env.NEXT_PUBLIC_URL_CARTOGRAPHY_BAN +const URL_CARTOGRAPHY_BAN = env('NEXT_PUBLIC_URL_CARTOGRAPHY_BAN') interface MicroToponymAddressListProps { microToponym: TypeMicroToponymExtended diff --git a/src/app/carte-base-adresse-nationale/layout.tsx b/src/app/carte-base-adresse-nationale/layout.tsx index 846b5d194..43c8d7fde 100644 --- a/src/app/carte-base-adresse-nationale/layout.tsx +++ b/src/app/carte-base-adresse-nationale/layout.tsx @@ -9,6 +9,8 @@ import { BALWidgetContext } from '@/contexts/BALWidget.context' import { BanMapProvider, useBanMapConfig } from './components/ban-map/BanMap.context' import { theme } from './components/ban-map/theme' import Legend from './components/Legend' +import { PublicEnvScript } from 'next-runtime-env' + import { CartoWrapper, CartoMenu, @@ -92,6 +94,7 @@ export default function RootLayout({ children }: { children: JSX.Element }) { return ( + { children } diff --git a/src/app/carte-base-adresse-nationale/page.tsx b/src/app/carte-base-adresse-nationale/page.tsx index fe3773c48..44d57b442 100644 --- a/src/app/carte-base-adresse-nationale/page.tsx +++ b/src/app/carte-base-adresse-nationale/page.tsx @@ -33,6 +33,7 @@ import { useRouter } from 'next/navigation' import { useBanMapConfig } from './components/ban-map/BanMap.context' import type { Address } from './components/ban-map/types' +import { env } from 'next-runtime-env' interface LinkProps { href: string @@ -43,7 +44,7 @@ type MapBreadcrumbPathSegment = string | { label: string, linkProps?: LinkProps const DEFAULT_CENTER = [1.7, 46.9] const DEFAULT_ZOOM = 6 const DEFAULT_URL_DISTRICT_FLAG = '/commune/default-logo.svg' -const URL_CARTOGRAPHY_BAN = process.env.NEXT_PUBLIC_URL_CARTOGRAPHY_BAN +const URL_CARTOGRAPHY_BAN = env('NEXT_PUBLIC_URL_CARTOGRAPHY_BAN') const getBanItemTypes = (banItem?: { type: 'commune' | 'voie' | 'lieu-dit' | 'numero' }) => { switch (banItem?.type) { diff --git a/src/app/certificat/[idCertificat]/page.tsx b/src/app/certificat/[idCertificat]/page.tsx index 90b7028ba..66a878a47 100644 --- a/src/app/certificat/[idCertificat]/page.tsx +++ b/src/app/certificat/[idCertificat]/page.tsx @@ -7,8 +7,9 @@ import { FieldLabel, FieldValue, } from './page.styles' +import { env } from 'next-runtime-env' -const { NEXT_PUBLIC_API_BAN_URL } = process.env +const NEXT_PUBLIC_API_BAN_URL = env('NEXT_PUBLIC_API_BAN_URL') async function Certificat({ params }: { params: { idCertificat: string } }) { const { idCertificat } = params diff --git a/src/app/commune/[codeCommune]/page.styles.tsx b/src/app/commune/[codeCommune]/page.styles.tsx index cdc9613bf..f0b598e23 100644 --- a/src/app/commune/[codeCommune]/page.styles.tsx +++ b/src/app/commune/[codeCommune]/page.styles.tsx @@ -13,6 +13,7 @@ export const StyledCommunePage = styled.div<{ $certificationPercentage: number } margin-bottom: 2rem; .commune-general-info { + line-height: normal; display: flex; flex-direction: column; align-items: center; diff --git a/src/app/commune/[codeCommune]/page.tsx b/src/app/commune/[codeCommune]/page.tsx index 2b230d0e5..ab1897f4f 100644 --- a/src/app/commune/[codeCommune]/page.tsx +++ b/src/app/commune/[codeCommune]/page.tsx @@ -5,7 +5,6 @@ import { Table } from '@codegouvfr/react-dsfr/Table' import CardWrapper from '@/components/CardWrapper' import Section from '@/components/Section' import { - getBanItem, getDistrict, getCommune as getBANCommune, assemblageSources, @@ -14,7 +13,7 @@ import { formatFr } from '@/lib/array' import { getRevisionDetails, getRevisions } from '@/lib/api-depot' import { getMairiePageURL } from '@/lib/api-etablissement-public' import { getCommune as getAPIGeoCommune, getEPCI } from '@/lib/api-geo' -import { getCommuneFlag } from '@/lib/api-wikidata' +import { getCommuneFlag } from '@/lib/api-blasons-communes' import { CommuneDownloadSection } from '../../../components/Commune/CommuneDownloadSection' import { CommuneNavigation } from '../../../components/Commune/CommuneNavigation' diff --git a/src/app/deploiement-bal/page.tsx b/src/app/deploiement-bal/page.tsx index 7ccd53232..32922ae2f 100644 --- a/src/app/deploiement-bal/page.tsx +++ b/src/app/deploiement-bal/page.tsx @@ -1,39 +1,21 @@ -import { getDepartements, getEPCI } from '@/lib/api-geo' -import departementCenterMap from '@/data/departement-center.json' +import { getEPCI } from '@/lib/api-geo' +import departements from '@/data/departement-center.json' import { getStats } from '@/lib/api-ban' import DeploiementBALDashboard from '../../components/DeploiementBAL/DeploiementBALDashboard' import Section from '@/components/Section' import { mapToSearchResult } from '@/lib/deploiement-stats' - -export type DeploiementBALSearchResultEPCI = { - type: 'EPCI' - code: string - nom: string - center: { type: string, coordinates: [number, number] } - contour: { type: string, coordinates: number[][][] } -} - -export type DeploiementBALSearchResultDepartement = { - type: 'Département' - code: string - nom: string - center: { type: string, coordinates: [number, number] } -} - -export type DeploiementBALSearchResult = DeploiementBALSearchResultEPCI | DeploiementBALSearchResultDepartement +import { DeploiementBALSearchResult } from '@/hooks/useStatsDeploiement' +import { Departement } from '@/types/api-geo.types' export default async function DeploiementBALPage({ searchParams }: { searchParams: Record }) { const stats = await getStats() - const departements = await getDepartements() - const departementsWithCenter = departements.map(({ code, ...rest }) => { - const { geometry } = (departementCenterMap as any)[code] - + const departementsWithCenter = Object.values(departements).map(({ properties, geometry }) => { return { - ...rest, - code, + ...properties, + type: 'Département', centre: geometry, } - }) + }) as (Departement & { centre: { type: string, coordinates: number[] } })[] let initialFilter: DeploiementBALSearchResult | null = null if (searchParams.departement) { diff --git a/src/app/documentation-bal/page.styles.ts b/src/app/documentation-bal/page.styles.ts new file mode 100644 index 000000000..f488d1e0d --- /dev/null +++ b/src/app/documentation-bal/page.styles.ts @@ -0,0 +1,161 @@ +'use client' + +import styled from 'styled-components' + +export const StyledPage = styled.div` +.on-this-page { + display: flex; + align-items: center; + > div { + flex: 1; + padding: 2rem; + + } + .text-wrapper { + ul { + margin: 2rem 0; + padding: 2rem 0; + border: 1px solid ${({ theme }) => theme.colors.grey.border}; + border-left: none; + border-right: none; + } + + .logo-wrapper { + display: flex; + justify-content: space-around ; + + > .ban-logo { + max-width: 180px; + max-height: 80px; + } + > .bal-logo { + max-width: 140px; + max-height: 80px; + } + } + } +} + +.stay-tuned { + padding-bottom: 0 !important; + > div { + display: flex; + + > .text-wrapper { + text-align: right; + margin-right: 4rem; + + > i::before { + height: 2.5rem; + width: 2.5rem; + margin-bottom: 1rem; + } + } + + > .illustration-wrapper { + height: 310px; + width: 470px; + flex-shrink: 0; + position: relative; + + > img { + position: absolute; + bottom: -38px; + } + } + } + +} + +.guide-section { + > div { + display: flex; + align-items: center; + + > .text-wrapper { + flex-basis: 60%; + + > button { + margin: 2rem 0; + } + > div { + margin-bottom: 2rem; + > h2 { + font-size: 1.5rem; + margin-bottom: 0.1rem; + } + > legend { + font-size: 0.8rem; + color: ${({ theme }) => theme.colors.grey.main}; + + } + } + > .buttons-wrapper { + display: flex; + align-items: center; + > a { + margin-right: 1rem; + } + } + } + + > .illustration-wrapper { + padding: 2rem; + flex-basis: 40%; + display: flex; + justify-content: center; + } + } +} + +@media screen and (max-width: ${props => props.theme.breakpoints.md}) { + .illustration-wrapper { + display: none; + } + + .on-this-page { + .text-wrapper { + padding: 0; + + .logo-wrapper { + > .ban-logo { + max-width: 90px; + max-height: 40px; + } + > .bal-logo { + max-width: 70px; + max-height: 40px; + } + } + } + } + + .stay-tuned { + padding-bottom: 1.5rem !important; + > div { + display: flex; + + > .text-wrapper { + text-align: initial; + margin-right: 0; + } + } + } + + .guide-section { + > div { + flex-direction: column; + + > .text-wrapper { + flex-basis: 100%; + order: 1; + } + + > .illustration-wrapper { + flex-basis: 100%; + order: 2; + } + } + } +} +` diff --git a/src/app/documentation-bal/page.tsx b/src/app/documentation-bal/page.tsx new file mode 100644 index 000000000..27a13937c --- /dev/null +++ b/src/app/documentation-bal/page.tsx @@ -0,0 +1,156 @@ +'use client' + +import ResponsiveImage from '@/components/ResponsiveImage' +import Section from '@/components/Section' +import { StyledPage } from './page.styles' +import SectionTilesList from '@/components/SectionTilesList' +import Button from '@codegouvfr/react-dsfr/Button' +import { useContext } from 'react' +import BALWidgetContext from '@/contexts/BALWidget.context' +import DownloadGuideCard from '@/components/DownloadGuideCard' + +const ressourcesData = [ + { + title: 'La documentation', + description: 'Cette documentation vous fournit les informations relatives à la Base Adresse Nationale, au format Base Adresse Locale, ainsi que des FAQ et conseils pratiques.', + picto: '/documentation-bal/book.png', + link: { + href: 'https://doc.adresse.data.gouv.fr/', + target: '_self', + }, + }, + { + title: 'Les guides', + description: 'Pour vous accompagner dans la gestion des adresses de votre commune, vous trouverez sur cette page des guides régulièrement mis à jour.', + picto: '/documentation-bal/document-download.png', + link: { + href: '#guide-mes-adresses', + }, + }, + { + title: 'La FAQ', + description: 'La F.A.Q répond aux questions les plus courantes, posées lors des webinaires par les acteurs de la commune.', + picto: '/documentation-bal/community.png', + link: { + href: 'https://adresse-data-gouv-fr.gitbook.io/faq', + target: '_self', + }, + }, +] + +export default function RessourcesPage() { + const { open, navigate } = useContext(BALWidgetContext) + + const handleOpenContactForm = () => { + navigate('/commune/contact') + open() + } + + return ( + +
+
+
+ +
+
+
+ Pour vous accompagner dans la gestion des adresses de votre commune, vous trouverez sur cette page: +
+
    +
  • + Des guides +
    Régulièrement mis à jour.
    +
  • +
  • + La documentation +
    Présentant la Base Adresse Nationale, les formats d’adresse ainsi que les services et outils accessibles sur le site.
    +
  • +
  • + Une FAQ +
    Répondant aux questions les plus courantes.
    +
  • +
+
+ + +
+
+
+
+ +
+
+
+ +

Restez informés

+

+ Pour être tenu informé des mises à jour ou suggérer des évolutions, n’hésitez-pas à nous contacter. +

+ +
+
+ +
+
+
+
+
+
+
+

+ Le guide de "Mes Adresses" +

+ + Version 8.2 - 01/06/2023 + +
+

+ "Mes Adresses" est un outil en ligne qui vous permet de gérer simplement vos adresses, de la constitution d’une Base Adresse Locale à sa mise à jour. Il est accessible sans compétences techniques et dispose d’un tutoriel embarqué. +

+
+ + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+

+ Le guide des bonnes pratiques +

+ à l’usage des communes et de leurs partenaires + + Version 4.1 - 08/09/2023 + +
+

+ Les communes sont responsables de leurs adresses. Ce guide passe en revue les bonnes pratiques pour nommer, numéroter les voies et diffuser l’information en parfaite conformité avec les obligations légales et rien que les obligations légales. +

+
+ + +
+
+
+
+
+ ) +} diff --git a/src/app/evenements/page.tsx b/src/app/evenements/page.tsx index d509fd026..12c57df03 100644 --- a/src/app/evenements/page.tsx +++ b/src/app/evenements/page.tsx @@ -6,6 +6,8 @@ import EventCard from '@/components/Events/EventCard' import { EventType } from '@/types/events.types' import { getUpcomingAndPassedEvents, mapEvents } from '@/utils/events' +export const dynamic = 'force-dynamic' + export default async function EvenementsPage() { const balEvents = await getBalEvents() diff --git a/src/app/formation-en-ligne/page.tsx b/src/app/formation-en-ligne/page.tsx index 1f54fc511..c47c66c25 100644 --- a/src/app/formation-en-ligne/page.tsx +++ b/src/app/formation-en-ligne/page.tsx @@ -4,6 +4,7 @@ import Section from '@/components/Section' import VideoMiniature from '@/components/VideoMiniature' import { getBalEvents } from '@/lib/api-bal-admin' import { getUpcomingAndPassedEvents, mapEvents } from '@/utils/events' +export const dynamic = 'force-dynamic' const videoFormations = [ { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b50b3a36e..0b2fcc881 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,6 +17,7 @@ import GlobalStyle from './global.styles' import { useEffect } from 'react' import { init as matomoInit } from '@socialgouv/matomo-next' import { BALWidgetProvider } from '@/contexts/BALWidget.context' +import { PublicEnvScript, env } from 'next-runtime-env' const StyledLayout = styled.div` min-height: 100vh; @@ -39,16 +40,17 @@ export default function RootLayout({ children }: { children: JSX.Element }) { } useEffect(() => { - if (!process.env.NEXT_PUBLIC_MATOMO_URL || !process.env.NEXT_PUBLIC_MATOMO_SITE_ID) { + if (!env('NEXT_PUBLIC_MATOMO_URL') || !env('NEXT_PUBLIC_MATOMO_SITE_ID')) { return } - matomoInit({ url: process.env.NEXT_PUBLIC_MATOMO_URL, siteId: process.env.NEXT_PUBLIC_MATOMO_SITE_ID }) + matomoInit({ url: env('NEXT_PUBLIC_MATOMO_URL') || '', siteId: env('NEXT_PUBLIC_MATOMO_SITE_ID') || '' }) }, []) return ( + diff --git a/src/components/DeploiementBAL/DeploiementBALDashboard.tsx b/src/components/DeploiementBAL/DeploiementBALDashboard.tsx index 1dc5ecc65..91ebac969 100644 --- a/src/components/DeploiementBAL/DeploiementBALDashboard.tsx +++ b/src/components/DeploiementBAL/DeploiementBALDashboard.tsx @@ -1,7 +1,7 @@ 'use client' import AutocompleteInput from '@/components/Autocomplete/AutocompleteInput' -import { useStatsDeploiement } from '@/hooks/useStatsDeploiement' +import { DeploiementBALSearchResult, useStatsDeploiement } from '@/hooks/useStatsDeploiement' import { getEpcis } from '@/lib/api-geo' import { BANStats } from '@/types/api-ban.types' import { Departement } from '@/types/api-geo.types' @@ -12,7 +12,6 @@ import { StyledDeploiementBALDashboard } from './DeploiementBALDashboard.styles' import { Tabs } from '@codegouvfr/react-dsfr/Tabs' import TabMesAdresses from './TabMesAdresses' import DeploiementMap, { getStyle } from './DeploiementMap' -import { DeploiementBALSearchResult } from '@/app/deploiement-bal/page' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { mapToSearchResult } from '@/lib/deploiement-stats' import { FullScreenControl } from '../Map/FullScreenControl' @@ -20,7 +19,7 @@ import { FullScreenControl } from '../Map/FullScreenControl' interface DeploiementBALMapProps { initialStats: BANStats initialFilter: DeploiementBALSearchResult | null - departements: (Departement & { centre: { type: string, coordinates: [number, number] } })[] + departements: (Departement & { centre: { type: string, coordinates: number[] } })[] } export default function DeploiementBALMap({ initialStats, initialFilter, departements }: DeploiementBALMapProps) { diff --git a/src/components/DeploiementBAL/DeploiementMap.tsx b/src/components/DeploiementBAL/DeploiementMap.tsx index cbe02a43e..97d86619d 100644 --- a/src/components/DeploiementBAL/DeploiementMap.tsx +++ b/src/components/DeploiementBAL/DeploiementMap.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react' import styled from 'styled-components' import { MapMouseEvent, Popup, useMap } from 'react-map-gl/maplibre' import { toolsColors } from '@/theme/theme' -import Link from 'next/link' const StyledWrapper = styled.div` .legend-wrapper { @@ -288,7 +287,7 @@ export default function DeploiementMap({ center, zoom, filteredCodesCommmune, se
Nombre d’adresses : {popup.properties.nbNumeros}
{`${popup.properties.certificationPercentage ? `Taux de certification : ${popup.properties.certificationPercentage}%` : 'Aucune adresse n’est certifiée par la commune'}`}
{popup.properties.hasBAL ?
Déposé via : {popup.properties.nomClient}
:
Ne dispose pas d'une BAL
} - Voir la page commune + Voir la page commune )} diff --git a/src/components/DeploiementBAL/TabDeploiementBAL.tsx b/src/components/DeploiementBAL/TabDeploiementBAL.tsx index d8b9b753f..1142ad4f9 100644 --- a/src/components/DeploiementBAL/TabDeploiementBAL.tsx +++ b/src/components/DeploiementBAL/TabDeploiementBAL.tsx @@ -1,7 +1,7 @@ 'use client' -import { DeploiementBALSearchResult } from '@/app/deploiement-bal/page' import DoughnutCounter from '@/components/ChartJS/DoughnutCounter' +import { DeploiementBALSearchResult } from '@/hooks/useStatsDeploiement' import { customFetch } from '@/lib/fetch' import { BANStats } from '@/types/api-ban.types' import { numFormater } from '@/utils/number' diff --git a/src/components/DownloadGuideCard/index.tsx b/src/components/DownloadGuideCard/index.tsx new file mode 100644 index 000000000..4d8d5d2d7 --- /dev/null +++ b/src/components/DownloadGuideCard/index.tsx @@ -0,0 +1,52 @@ +'use client' + +import { useState } from 'react' +import styled from 'styled-components' +import ResponsiveImage, { ResponsiveImageProps } from '../ResponsiveImage' + +export interface DownloadGuideCardProps { + imgProps: ResponsiveImageProps + downloadlink: string + onDownloadStart?: () => void +} + +const StyledWrapper = styled.div` + position: relative; + display: flex; + cursor: pointer; + + > .hover-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.2); + display: flex; + justify-content: center; + align-items: center; + color: white; + font-size: 2rem; + } +` + +export default function DownloadGuideCard({ + imgProps, + downloadlink, + onDownloadStart, +}: DownloadGuideCardProps) { + const [isHovered, setIsHovered] = useState(false) + const handleClick = () => { + if (onDownloadStart) { + onDownloadStart() + } + window.open(downloadlink, '_blank') + } + + return ( + setIsHovered(true)} onMouseLeave={() => { setIsHovered(false) }}> + {isHovered &&
} + +
+ ) +} diff --git a/src/components/Events/EventCard.styles.ts b/src/components/Events/EventCard.styles.ts index d3686a653..430d80ace 100644 --- a/src/components/Events/EventCard.styles.ts +++ b/src/components/Events/EventCard.styles.ts @@ -2,10 +2,11 @@ import styled from 'styled-components' -export const StyledEventCard = styled.div<{ $isPassed?: boolean }>` +export const StyledEventCard = styled.div<{ $isPassed?: boolean, $backgroundColor?: string }>` padding: 32px; border: 1px solid ${({ theme }) => theme.colors.grey.border}; ${({ $isPassed }) => $isPassed && 'opacity: 0.5;'} + ${({ $backgroundColor }) => $backgroundColor && `background-color: ${$backgroundColor};`} .event-details { color: ${({ theme }) => theme.colors.primary.main}; diff --git a/src/components/Events/EventCard.tsx b/src/components/Events/EventCard.tsx index c54380293..09f30991d 100644 --- a/src/components/Events/EventCard.tsx +++ b/src/components/Events/EventCard.tsx @@ -1,6 +1,6 @@ 'use client' -import { EventType } from '@/types/events.types' +import { EventType, EventTypeTypeEnum } from '@/types/events.types' import { StyledEventCard } from './EventCard.styles' import Badge from '@codegouvfr/react-dsfr/Badge' import { getFullDate } from '@/utils/date' @@ -12,12 +12,18 @@ interface EventCardProps { isPassed?: boolean } +const backgroundColors: Record = { + [EventTypeTypeEnum.FORMATION]: 'rgba(136, 213, 156, 0.1)', + [EventTypeTypeEnum.FORMATION_LVL2]: 'rgba(136, 213, 156, 0.25)', +} + export default function EventCard({ event, isPassed, tagToColor }: EventCardProps) { - const { tags, title, description, startHour, endHour, date, address, isOnlineOnly, isSubscriptionClosed, href } = event + const { tags, title, description, startHour, endHour, date, address, isSubscriptionClosed, href, type } = event - const hasLargeDescription = description.length > 200 + const hasLargeDescription = description.length > 100 const [showAllDescription, setShowAllDescription] = useState(!hasLargeDescription) - const actualDescription = showAllDescription ? description : description.slice(0, 200) + '...' + const actualDescription = showAllDescription ? description : description.slice(0, 100) + '...' + const backgroundColor = backgroundColors[type] const getAdressToString = (adress: EventType['address']) => { if (!adress) return '' @@ -25,7 +31,7 @@ export default function EventCard({ event, isPassed, tagToColor }: EventCardProp } return ( - +
<>{tags.map(tag => {tag})}
@@ -34,7 +40,7 @@ export default function EventCard({ event, isPassed, tagToColor }: EventCardProp {getFullDate(new Date(date))} {' | '} {startHour} - {endHour} - {isOnlineOnly ? | En ligne : address ? | {getAdressToString(address)} : null} + {address?.commune ? | {getAdressToString(address)} : null}

{title}

diff --git a/src/components/FormulaireDePublication/index.tsx b/src/components/FormulaireDePublication/index.tsx index d48b730d7..6146c54eb 100644 --- a/src/components/FormulaireDePublication/index.tsx +++ b/src/components/FormulaireDePublication/index.tsx @@ -12,7 +12,7 @@ import { Commune } from '@/types/api-geo.types' import { validateHabilitationPinCode, createHabilitation, createRevision, getHabilitation, sendHabilitationPinCode, getCurrentRevision, publishRevision } from '@/lib/api-depot' import Alert from '@codegouvfr/react-dsfr/Alert' import Image from 'next/image' -import { getCommuneFlag } from '@/lib/api-wikidata' +import { getCommuneFlag } from '@/lib/api-blasons-communes' import { HabilitationMethod } from './steps/HablitationMethod' import { Habilitation, HabilitationStatus, Revision } from '@/types/api-depot.types' import { PinCodeValidation } from './steps/PinCodeValidation' diff --git a/src/components/NewsletterOptin/index.tsx b/src/components/NewsletterOptin/index.tsx index 6fc0a1623..facaaa8c5 100644 --- a/src/components/NewsletterOptin/index.tsx +++ b/src/components/NewsletterOptin/index.tsx @@ -2,6 +2,9 @@ import 'altcha' import { useCallback, useEffect, useState } from 'react' +import { env } from 'next-runtime-env' + +const ALTCHA_API_KEY = env('NEXT_PUBLIC_ALTCHA_API_KEY') interface LoaderProps { children: React.ReactNode @@ -43,7 +46,7 @@ export default function NewsletterOptin({ children, handleSubmit, onVerified, sh {showCatpcha && ( diff --git a/src/components/Partenaires/moissonneur-revision/MoissonneurRevisionItem.tsx b/src/components/Partenaires/moissonneur-revision/MoissonneurRevisionItem.tsx index 3f442c98a..f98257b09 100644 --- a/src/components/Partenaires/moissonneur-revision/MoissonneurRevisionItem.tsx +++ b/src/components/Partenaires/moissonneur-revision/MoissonneurRevisionItem.tsx @@ -6,18 +6,19 @@ import UpdateStatusBadge from '../UpdateStatus' import { getFileLink } from '@/lib/api-moissonneur-bal' import Button from '@codegouvfr/react-dsfr/Button' import { PublicationMoissoneurType, RevisionMoissoneurType } from '@/types/api-moissonneur-bal.types' +import { env } from 'next-runtime-env' const otherClients = [ { - id: process.env.NEXT_PUBLIC_CLIENT_GUICHET_ADRESSE, + id: env('NEXT_PUBLIC_CLIENT_GUICHET_ADRESSE'), name: 'Guichet Adresse', }, { - id: process.env.NEXT_PUBLIC_CLIENT_MES_ADRESSE, + id: env('NEXT_PUBLIC_CLIENT_MES_ADRESSE'), name: 'Mes Adresses', }, { - id: process.env.NEXT_PUBLIC_CLIENT_FORMULAIRE_PUBLICATION, + id: env('NEXT_PUBLIC_CLIENT_FORMULAIRE_PUBLICATION'), name: 'Formulaire Publication', }, ] diff --git a/src/components/ResponsiveImage/index.tsx b/src/components/ResponsiveImage/index.tsx index 1196d6072..c85cd7699 100644 --- a/src/components/ResponsiveImage/index.tsx +++ b/src/components/ResponsiveImage/index.tsx @@ -4,15 +4,17 @@ export interface ResponsiveImageProps { src: string alt: string style?: React.CSSProperties + className?: string } -export default function ResponsiveImage({ src, alt, style }: ResponsiveImageProps) { +export default function ResponsiveImage({ src, alt, style, className }: ResponsiveImageProps) { return ( {alt} diff --git a/src/components/SearchBAN/SearchBAN.tsx b/src/components/SearchBAN/SearchBAN.tsx index 282859894..8504d4f9a 100644 --- a/src/components/SearchBAN/SearchBAN.tsx +++ b/src/components/SearchBAN/SearchBAN.tsx @@ -7,8 +7,9 @@ import { search, isFirstCharValid } from '@/lib/api-adresse' import { getCommune as getCommuneByINSEE } from '@/lib/api-ban' import SearchInput from './search-input' +import { env } from 'next-runtime-env' -const URL_CARTOGRAPHY_BAN = process.env.NEXT_PUBLIC_URL_CARTOGRAPHY_BAN +const URL_CARTOGRAPHY_BAN = env('NEXT_PUBLIC_URL_CARTOGRAPHY_BAN') const featuresTypes = { municipality: 'Communes ou Arrondissements', diff --git a/src/components/SectionSearchBAN/SectionSearchBAN.tsx b/src/components/SectionSearchBAN/SectionSearchBAN.tsx index a91862d92..935975bf8 100644 --- a/src/components/SectionSearchBAN/SectionSearchBAN.tsx +++ b/src/components/SectionSearchBAN/SectionSearchBAN.tsx @@ -16,8 +16,9 @@ import { FormDescription, ButtonLink, } from './SectionSearchBAN.styles' +import { env } from 'next-runtime-env' -const URL_CARTOGRAPHY_BAN = process.env.NEXT_PUBLIC_URL_CARTOGRAPHY_BAN +const URL_CARTOGRAPHY_BAN = env('NEXT_PUBLIC_URL_CARTOGRAPHY_BAN') interface SectionSearchBANProps { id?: string diff --git a/src/contexts/BALWidget.context.tsx b/src/contexts/BALWidget.context.tsx index 59c5c0755..7ba062c56 100644 --- a/src/contexts/BALWidget.context.tsx +++ b/src/contexts/BALWidget.context.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState, createContext } from 'react' import styled, { css } from 'styled-components' import { matomoTrackEvent } from '@/lib/matomo' +import { env } from 'next-runtime-env' export const StyledIFrame = styled.iframe<{ $isOpen: boolean, $isVisible: boolean }>` position: fixed; @@ -97,7 +98,7 @@ export function BALWidgetProvider({ children }: BALWidgetProviderProps) { useEffect(() => { async function fetchBalWidgetConfig() { try { - const response = await fetch(`${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/bal-widget/config`) + const response = await fetch(`${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/bal-widget/config`) const data = await response.json() if (response.status !== 200) { throw new Error(data.message) @@ -187,7 +188,7 @@ export function BALWidgetProvider({ children }: BALWidgetProviderProps) { {isWidgetDisplayed && ( diff --git a/src/data/departement-center.json b/src/data/departement-center.json index 6169af364..2cb4dc603 100644 --- a/src/data/departement-center.json +++ b/src/data/departement-center.json @@ -4,7 +4,7 @@ "properties": { "code": "01", "nom": "Ain", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -16,7 +16,7 @@ "properties": { "code": "02", "nom": "Aisne", - "region": "32" + "codeRegion": "32" }, "geometry": { "type": "Point", @@ -28,7 +28,7 @@ "properties": { "code": "03", "nom": "Allier", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -40,7 +40,7 @@ "properties": { "code": "04", "nom": "Alpes-de-Haute-Provence", - "region": "93" + "codeRegion": "93" }, "geometry": { "type": "Point", @@ -52,7 +52,7 @@ "properties": { "code": "05", "nom": "Hautes-Alpes", - "region": "93" + "codeRegion": "93" }, "geometry": { "type": "Point", @@ -64,7 +64,7 @@ "properties": { "code": "06", "nom": "Alpes-Maritimes", - "region": "93" + "codeRegion": "93" }, "geometry": { "type": "Point", @@ -76,7 +76,7 @@ "properties": { "code": "07", "nom": "Ardèche", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -88,7 +88,7 @@ "properties": { "code": "08", "nom": "Ardennes", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -100,7 +100,7 @@ "properties": { "code": "09", "nom": "Ariège", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -112,7 +112,7 @@ "properties": { "code": "10", "nom": "Aube", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -124,7 +124,7 @@ "properties": { "code": "11", "nom": "Aude", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -136,7 +136,7 @@ "properties": { "code": "12", "nom": "Aveyron", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -148,7 +148,7 @@ "properties": { "code": "13", "nom": "Bouches-du-Rhône", - "region": "93" + "codeRegion": "93" }, "geometry": { "type": "Point", @@ -160,7 +160,7 @@ "properties": { "code": "14", "nom": "Calvados", - "region": "28" + "codeRegion": "28" }, "geometry": { "type": "Point", @@ -172,7 +172,7 @@ "properties": { "code": "15", "nom": "Cantal", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -184,7 +184,7 @@ "properties": { "code": "16", "nom": "Charente", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -196,7 +196,7 @@ "properties": { "code": "17", "nom": "Charente-Maritime", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -208,7 +208,7 @@ "properties": { "code": "18", "nom": "Cher", - "region": "24" + "codeRegion": "24" }, "geometry": { "type": "Point", @@ -220,7 +220,7 @@ "properties": { "code": "19", "nom": "Corrèze", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -232,7 +232,7 @@ "properties": { "code": "21", "nom": "Côte-d'Or", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -244,7 +244,7 @@ "properties": { "code": "22", "nom": "Côtes-d'Armor", - "region": "53" + "codeRegion": "53" }, "geometry": { "type": "Point", @@ -256,7 +256,7 @@ "properties": { "code": "23", "nom": "Creuse", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -268,7 +268,7 @@ "properties": { "code": "24", "nom": "Dordogne", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -280,7 +280,7 @@ "properties": { "code": "25", "nom": "Doubs", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -292,7 +292,7 @@ "properties": { "code": "26", "nom": "Drôme", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -304,7 +304,7 @@ "properties": { "code": "27", "nom": "Eure", - "region": "28" + "codeRegion": "28" }, "geometry": { "type": "Point", @@ -316,7 +316,7 @@ "properties": { "code": "28", "nom": "Eure-et-Loir", - "region": "24" + "codeRegion": "24" }, "geometry": { "type": "Point", @@ -328,7 +328,7 @@ "properties": { "code": "29", "nom": "Finistère", - "region": "53" + "codeRegion": "53" }, "geometry": { "type": "Point", @@ -340,7 +340,7 @@ "properties": { "code": "2A", "nom": "Corse-du-Sud", - "region": "94" + "codeRegion": "94" }, "geometry": { "type": "Point", @@ -352,7 +352,7 @@ "properties": { "code": "2B", "nom": "Haute-Corse", - "region": "94" + "codeRegion": "94" }, "geometry": { "type": "Point", @@ -364,7 +364,7 @@ "properties": { "code": "30", "nom": "Gard", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -376,7 +376,7 @@ "properties": { "code": "31", "nom": "Haute-Garonne", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -388,7 +388,7 @@ "properties": { "code": "32", "nom": "Gers", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -400,7 +400,7 @@ "properties": { "code": "33", "nom": "Gironde", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -412,7 +412,7 @@ "properties": { "code": "34", "nom": "Hérault", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -424,7 +424,7 @@ "properties": { "code": "35", "nom": "Ille-et-Vilaine", - "region": "53" + "codeRegion": "53" }, "geometry": { "type": "Point", @@ -436,7 +436,7 @@ "properties": { "code": "36", "nom": "Indre", - "region": "24" + "codeRegion": "24" }, "geometry": { "type": "Point", @@ -448,7 +448,7 @@ "properties": { "code": "37", "nom": "Indre-et-Loire", - "region": "24" + "codeRegion": "24" }, "geometry": { "type": "Point", @@ -460,7 +460,7 @@ "properties": { "code": "38", "nom": "Isère", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -472,7 +472,7 @@ "properties": { "code": "39", "nom": "Jura", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -484,7 +484,7 @@ "properties": { "code": "40", "nom": "Landes", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -496,7 +496,7 @@ "properties": { "code": "41", "nom": "Loir-et-Cher", - "region": "24" + "codeRegion": "24" }, "geometry": { "type": "Point", @@ -508,7 +508,7 @@ "properties": { "code": "42", "nom": "Loire", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -520,7 +520,7 @@ "properties": { "code": "43", "nom": "Haute-Loire", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -532,7 +532,7 @@ "properties": { "code": "44", "nom": "Loire-Atlantique", - "region": "52" + "codeRegion": "52" }, "geometry": { "type": "Point", @@ -544,7 +544,7 @@ "properties": { "code": "45", "nom": "Loiret", - "region": "24" + "codeRegion": "24" }, "geometry": { "type": "Point", @@ -556,7 +556,7 @@ "properties": { "code": "46", "nom": "Lot", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -568,7 +568,7 @@ "properties": { "code": "47", "nom": "Lot-et-Garonne", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -580,7 +580,7 @@ "properties": { "code": "48", "nom": "Lozère", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -592,7 +592,7 @@ "properties": { "code": "49", "nom": "Maine-et-Loire", - "region": "52" + "codeRegion": "52" }, "geometry": { "type": "Point", @@ -604,7 +604,7 @@ "properties": { "code": "50", "nom": "Manche", - "region": "28" + "codeRegion": "28" }, "geometry": { "type": "Point", @@ -616,7 +616,7 @@ "properties": { "code": "51", "nom": "Marne", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -628,7 +628,7 @@ "properties": { "code": "52", "nom": "Haute-Marne", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -640,7 +640,7 @@ "properties": { "code": "53", "nom": "Mayenne", - "region": "52" + "codeRegion": "52" }, "geometry": { "type": "Point", @@ -652,7 +652,7 @@ "properties": { "code": "54", "nom": "Meurthe-et-Moselle", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -664,7 +664,7 @@ "properties": { "code": "55", "nom": "Meuse", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -676,7 +676,7 @@ "properties": { "code": "56", "nom": "Morbihan", - "region": "53" + "codeRegion": "53" }, "geometry": { "type": "Point", @@ -688,7 +688,7 @@ "properties": { "code": "57", "nom": "Moselle", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -700,7 +700,7 @@ "properties": { "code": "58", "nom": "Nièvre", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -712,7 +712,7 @@ "properties": { "code": "59", "nom": "Nord", - "region": "32" + "codeRegion": "32" }, "geometry": { "type": "Point", @@ -724,7 +724,7 @@ "properties": { "code": "60", "nom": "Oise", - "region": "32" + "codeRegion": "32" }, "geometry": { "type": "Point", @@ -736,7 +736,7 @@ "properties": { "code": "61", "nom": "Orne", - "region": "28" + "codeRegion": "28" }, "geometry": { "type": "Point", @@ -748,7 +748,7 @@ "properties": { "code": "62", "nom": "Pas-de-Calais", - "region": "32" + "codeRegion": "32" }, "geometry": { "type": "Point", @@ -760,7 +760,7 @@ "properties": { "code": "63", "nom": "Puy-de-Dôme", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -772,7 +772,7 @@ "properties": { "code": "64", "nom": "Pyrénées-Atlantiques", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -784,7 +784,7 @@ "properties": { "code": "65", "nom": "Hautes-Pyrénées", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -796,7 +796,7 @@ "properties": { "code": "66", "nom": "Pyrénées-Orientales", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -808,7 +808,7 @@ "properties": { "code": "67", "nom": "Bas-Rhin", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -820,7 +820,7 @@ "properties": { "code": "68", "nom": "Haut-Rhin", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -832,7 +832,7 @@ "properties": { "code": "69", "nom": "Rhône", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -844,7 +844,7 @@ "properties": { "code": "70", "nom": "Haute-Saône", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -856,7 +856,7 @@ "properties": { "code": "71", "nom": "Saône-et-Loire", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -868,7 +868,7 @@ "properties": { "code": "72", "nom": "Sarthe", - "region": "52" + "codeRegion": "52" }, "geometry": { "type": "Point", @@ -880,7 +880,7 @@ "properties": { "code": "73", "nom": "Savoie", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -892,7 +892,7 @@ "properties": { "code": "74", "nom": "Haute-Savoie", - "region": "84" + "codeRegion": "84" }, "geometry": { "type": "Point", @@ -904,7 +904,7 @@ "properties": { "code": "75", "nom": "Paris", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -916,7 +916,7 @@ "properties": { "code": "76", "nom": "Seine-Maritime", - "region": "28" + "codeRegion": "28" }, "geometry": { "type": "Point", @@ -928,7 +928,7 @@ "properties": { "code": "77", "nom": "Seine-et-Marne", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -940,7 +940,7 @@ "properties": { "code": "78", "nom": "Yvelines", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -952,7 +952,7 @@ "properties": { "code": "79", "nom": "Deux-Sèvres", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -964,7 +964,7 @@ "properties": { "code": "80", "nom": "Somme", - "region": "32" + "codeRegion": "32" }, "geometry": { "type": "Point", @@ -976,7 +976,7 @@ "properties": { "code": "81", "nom": "Tarn", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -988,7 +988,7 @@ "properties": { "code": "82", "nom": "Tarn-et-Garonne", - "region": "76" + "codeRegion": "76" }, "geometry": { "type": "Point", @@ -1000,7 +1000,7 @@ "properties": { "code": "83", "nom": "Var", - "region": "93" + "codeRegion": "93" }, "geometry": { "type": "Point", @@ -1012,7 +1012,7 @@ "properties": { "code": "84", "nom": "Vaucluse", - "region": "93" + "codeRegion": "93" }, "geometry": { "type": "Point", @@ -1024,7 +1024,7 @@ "properties": { "code": "85", "nom": "Vendée", - "region": "52" + "codeRegion": "52" }, "geometry": { "type": "Point", @@ -1036,7 +1036,7 @@ "properties": { "code": "86", "nom": "Vienne", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -1048,7 +1048,7 @@ "properties": { "code": "87", "nom": "Haute-Vienne", - "region": "75" + "codeRegion": "75" }, "geometry": { "type": "Point", @@ -1060,7 +1060,7 @@ "properties": { "code": "88", "nom": "Vosges", - "region": "44" + "codeRegion": "44" }, "geometry": { "type": "Point", @@ -1072,7 +1072,7 @@ "properties": { "code": "89", "nom": "Yonne", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -1084,7 +1084,7 @@ "properties": { "code": "90", "nom": "Territoire de Belfort", - "region": "27" + "codeRegion": "27" }, "geometry": { "type": "Point", @@ -1096,7 +1096,7 @@ "properties": { "code": "91", "nom": "Essonne", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -1108,7 +1108,7 @@ "properties": { "code": "92", "nom": "Hauts-de-Seine", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -1120,7 +1120,7 @@ "properties": { "code": "93", "nom": "Seine-Saint-Denis", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -1132,7 +1132,7 @@ "properties": { "code": "94", "nom": "Val-de-Marne", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -1144,7 +1144,7 @@ "properties": { "code": "95", "nom": "Val-d'Oise", - "region": "11" + "codeRegion": "11" }, "geometry": { "type": "Point", @@ -1156,7 +1156,7 @@ "properties": { "code": "971", "nom": "Guadeloupe", - "region": "01" + "codeRegion": "01" }, "geometry": { "type": "Point", @@ -1168,7 +1168,7 @@ "properties": { "code": "972", "nom": "Martinique", - "region": "02" + "codeRegion": "02" }, "geometry": { "type": "Point", @@ -1180,7 +1180,7 @@ "properties": { "code": "973", "nom": "Guyane", - "region": "03" + "codeRegion": "03" }, "geometry": { "type": "Point", @@ -1192,7 +1192,7 @@ "properties": { "code": "974", "nom": "La Réunion", - "region": "04" + "codeRegion": "04" }, "geometry": { "type": "Point", @@ -1204,7 +1204,7 @@ "properties": { "code": "976", "nom": "Mayotte", - "region": "06" + "codeRegion": "06" }, "geometry": { "type": "Point", @@ -1216,7 +1216,7 @@ "properties": { "code": "975", "nom": "Saint-Pierre-et-Miquelon", - "region": "975" + "codeRegion": "975" }, "geometry": { "type": "Point", @@ -1228,7 +1228,7 @@ "properties": { "code": "977", "nom": "Saint-Barthélemy", - "region": "977" + "codeRegion": "977" }, "geometry": { "type": "Point", @@ -1240,7 +1240,7 @@ "properties": { "code": "978", "nom": "Saint-Martin", - "region": "978" + "codeRegion": "978" }, "geometry": { "type": "Point", @@ -1252,7 +1252,7 @@ "properties": { "code": "984", "nom": "Terres australes et antarctiques françaises", - "region": "984" + "codeRegion": "984" }, "geometry": { "type": "Point", @@ -1264,7 +1264,7 @@ "properties": { "code": "986", "nom": "Wallis et Futuna", - "region": "986" + "codeRegion": "986" }, "geometry": { "type": "Point", @@ -1276,7 +1276,7 @@ "properties": { "code": "987", "nom": "Polynésie française", - "region": "987" + "codeRegion": "987" }, "geometry": { "type": "Point", @@ -1288,7 +1288,7 @@ "properties": { "code": "988", "nom": "Nouvelle-Calédonie", - "region": "988" + "codeRegion": "988" }, "geometry": { "type": "Point", @@ -1300,7 +1300,7 @@ "properties": { "code": "989", "nom": "Île de Clipperton", - "region": "989" + "codeRegion": "989" }, "geometry": { "type": "Point", diff --git a/src/hooks/useStatsDeploiement.ts b/src/hooks/useStatsDeploiement.ts index bb216a6a2..dc18e06ae 100644 --- a/src/hooks/useStatsDeploiement.ts +++ b/src/hooks/useStatsDeploiement.ts @@ -4,7 +4,23 @@ import { getEpciCommunes, getDepartementCommunes, getCommunes } from '@/lib/api- import { BANStats } from '@/types/api-ban.types' import { Commune } from '@/types/api-geo.types' import { DEFAULT_CENTER, DEFAULT_ZOOM } from '@/components/Map/map.config' -import { DeploiementBALSearchResult } from '@/app/deploiement-bal/page' + +export type DeploiementBALSearchResultEPCI = { + type: 'EPCI' + code: string + nom: string + center: { type: string, coordinates: [number, number] } + contour: { type: string, coordinates: number[][][] } +} + +export type DeploiementBALSearchResultDepartement = { + type: 'Département' + code: string + nom: string + center: { type: string, coordinates: [number, number] } +} + +export type DeploiementBALSearchResult = DeploiementBALSearchResultEPCI | DeploiementBALSearchResultDepartement const communesWithArrondissements = { 75056: 'Paris', 69123: 'Lyon', 13055: 'Marseille' } diff --git a/src/layouts/Footer.tsx b/src/layouts/Footer.tsx index 4b474a758..a04b76872 100644 --- a/src/layouts/Footer.tsx +++ b/src/layouts/Footer.tsx @@ -5,14 +5,13 @@ import { useCallback, useState } from 'react' import dynamic from 'next/dynamic' import Link from 'next/link' import { newsletterOptIn } from '@/lib/api-brevo' +import { env } from 'next-runtime-env' -const { - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_XCOM: SOCIAL_NETWORKS_URL_XCOM, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_MASTODON: SOCIAL_NETWORKS_URL_MASTODON, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_FACEBOOK: SOCIAL_NETWORKS_URL_FACEBOOK, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_LINKEDIN: SOCIAL_NETWORKS_URL_LINKEDIN, - NEXT_PUBLIC_SOCIAL_NETWORKS_URL_GITHUB: SOCIAL_NETWORKS_URL_GITHUB, -} = process.env +const SOCIAL_NETWORKS_URL_MASTODON = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_MASTODON') +const SOCIAL_NETWORKS_URL_FACEBOOK = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_FACEBOOK') +const SOCIAL_NETWORKS_URL_LINKEDIN = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_LINKEDIN') +const SOCIAL_NETWORKS_URL_GITHUB = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_GITHUB') +const SOCIAL_NETWORKS_URL_XCOM = env('NEXT_PUBLIC_SOCIAL_NETWORKS_URL_XCOM') const NewsletterOptinWithNoSSR = dynamic( () => import('../components/NewsletterOptin'), diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index b0c85944c..0904b3a15 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -9,8 +9,9 @@ import { Tooltip } from '@codegouvfr/react-dsfr/Tooltip' import Notices from '@/components/Notices' import { CornerRibbons } from './Header.styles' +import { env } from 'next-runtime-env' -const URL_CARTOGRAPHY_BAN = process.env.NEXT_PUBLIC_URL_CARTOGRAPHY_BAN +const URL_CARTOGRAPHY_BAN = env('NEXT_PUBLIC_URL_CARTOGRAPHY_BAN') const markAsActive = ( navigation: MainNavigationProps.Item[], @@ -63,6 +64,10 @@ export const navEntries: MainNavigationProps.Item[] = [ text: 'Consulter la page d’une commune', linkProps: { href: '/commune' }, }, + { + text: 'Documentation Base Adresse Locale', + linkProps: { href: '/documentation-bal' }, + }, { text: 'Application Mes adresses', linkProps: { diff --git a/src/lib/api-adresse.ts b/src/lib/api-adresse.ts index 73c0fcfb1..790eab9c2 100644 --- a/src/lib/api-adresse.ts +++ b/src/lib/api-adresse.ts @@ -1,3 +1,5 @@ +import { env } from 'next-runtime-env' + interface ExtendedError extends Error { code?: number url?: string @@ -14,7 +16,7 @@ class HttpError extends (Error as { new(message: string): ExtendedError }) { } } -const API_ADRESSE = process.env.NEXT_PUBLIC_API_ADRESSE +const API_ADRESSE = env('NEXT_PUBLIC_API_ADRESSE') if (!API_ADRESSE) { throw new Error('API_ADRESSE is not defined in the environment variables') diff --git a/src/lib/api-bal-admin.ts b/src/lib/api-bal-admin.ts index c19a7d10c..46db8c5ca 100644 --- a/src/lib/api-bal-admin.ts +++ b/src/lib/api-bal-admin.ts @@ -1,13 +1,14 @@ import { CandidatePartenaireDeLaCharteType, PartenaireDeLaCharteTypeEnum, PartenaireDeLaChartType } from '@/types/partenaire.types' import { customFetch } from './fetch' import { EventType } from '@/types/events.types' +import { env } from 'next-runtime-env' -if (!process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL) { +if (!env('NEXT_PUBLIC_BAL_ADMIN_API_URL')) { throw new Error('NEXT_PUBLIC_BAL_ADMIN_API_URL is not defined in the environment') } export async function getOnePartenairesDeLaCharte(id: string): Promise { - const url = new URL(`${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/partenaires-de-la-charte/${id}`) + const url = new URL(`${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/partenaires-de-la-charte/${id}`) return customFetch(url) } @@ -30,7 +31,7 @@ export interface PaginatedPartenairesDeLaCharte { export const DEFAULT_PARTENAIRES_DE_LA_CHARTE_LIMIT = 8 export async function getPartenairesDeLaCharte(queryObject: PartenairesDeLaCharteQuery, page: number = 1, limit: number = DEFAULT_PARTENAIRES_DE_LA_CHARTE_LIMIT): Promise<{ total: number, totalCommunes: number, totalOrganismes: number, totalEntreprises: number, data: PartenaireDeLaChartType[] }> { - const url = new URL(`${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/partenaires-de-la-charte/paginated`) + const url = new URL(`${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/partenaires-de-la-charte/paginated`) url.searchParams.append('page', page.toString()) url.searchParams.append('limit', limit.toString()) Object.keys(queryObject).forEach(key => url.searchParams.append(key, queryObject[key as keyof PartenairesDeLaCharteQuery] as string)) @@ -39,11 +40,11 @@ export async function getPartenairesDeLaCharte(queryObject: PartenairesDeLaChart } export async function getPartenairesDeLaCharteServices(): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/partenaires-de-la-charte/services`) + return customFetch(`${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/partenaires-de-la-charte/services`) } export async function getRandomPartenairesDeLaCharte(limit: number) { - const url = new URL(`${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/partenaires-de-la-charte/random`) + const url = new URL(`${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/partenaires-de-la-charte/random`) if (limit) { url.searchParams.append('limit', limit.toString()) } @@ -52,7 +53,7 @@ export async function getRandomPartenairesDeLaCharte(limit: number) { } export async function candidateToPartenairesDeLaCharte(candidacy: CandidatePartenaireDeLaCharteType) { - const request = `${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/partenaires-de-la-charte/candidate` + const request = `${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/partenaires-de-la-charte/candidate` return customFetch(request, { method: 'POST', @@ -65,7 +66,7 @@ export async function candidateToPartenairesDeLaCharte(candidacy: CandidateParte export async function getBalEvents(): Promise { try { - const response = await fetch(`${process.env.NEXT_PUBLIC_BAL_ADMIN_API_URL}/events`) + const response = await fetch(`${env('NEXT_PUBLIC_BAL_ADMIN_API_URL')}/events`) if (!response.ok) { throw new Error('Error while fetching bal events') } diff --git a/src/lib/api-ban.ts b/src/lib/api-ban.ts index fbff28c84..bd1876b1b 100644 --- a/src/lib/api-ban.ts +++ b/src/lib/api-ban.ts @@ -1,8 +1,9 @@ import { BANCommune, BANVoie, BANAddress } from '@/types/api-ban.types' import { BANStats } from '@/types/api-ban.types' import { customFetch } from './fetch' +import { env } from 'next-runtime-env' -const API_BAN_SEARCH_URL = process.env.NEXT_PUBLIC_API_BAN_URL +const API_BAN_SEARCH_URL = env('NEXT_PUBLIC_API_BAN_URL') if (!API_BAN_SEARCH_URL) { throw new Error('NEXT_PUBLIC_API_BAN_URL is not defined') diff --git a/src/lib/api-blasons-communes.ts b/src/lib/api-blasons-communes.ts new file mode 100644 index 000000000..d6e3b8ae7 --- /dev/null +++ b/src/lib/api-blasons-communes.ts @@ -0,0 +1,24 @@ +const BASE_URL = 'https://base-adresse-locale-prod-blasons-communes.s3.fr-par.scw.cloud' + +const DEFAULT_URL_DISTRICT_FLAG = '/commune/default-logo.svg' + +// Fetch the commune flag from a proxy for front-end to avoid CORS issues +export const getCommuneFlagProxy = async (codeCommune: string): Promise => { + const response = await fetch(`/api/proxy-flag-commune/${codeCommune}`) + + return response.json() +} + +export const getCommuneFlag = async (codeCommune: string): Promise => { + const url = `${BASE_URL}/${codeCommune}.svg` + + const response = await fetch(url, { + method: 'HEAD', + }) + + if (!response.ok) { + return DEFAULT_URL_DISTRICT_FLAG + } + + return url +} diff --git a/src/lib/api-data-gouv.ts b/src/lib/api-data-gouv.ts index af9f74d7d..67714d111 100644 --- a/src/lib/api-data-gouv.ts +++ b/src/lib/api-data-gouv.ts @@ -1,9 +1,10 @@ import { customFetch } from './fetch' +import { env } from 'next-runtime-env' -if (!process.env.NEXT_PUBLIC_DATAGOUV_URL) { +if (!env('NEXT_PUBLIC_DATAGOUV_URL')) { throw new Error('NEXT_PUBLIC_DATAGOUV_URL is not defined') } export function getDataset(datasetId: string) { - return customFetch(`${process.env.NEXT_PUBLIC_DATAGOUV_URL}/datasets/${datasetId}`) + return customFetch(`${env('NEXT_PUBLIC_DATAGOUV_URL')}/datasets/${datasetId}`) } diff --git a/src/lib/api-depot.tsx b/src/lib/api-depot.tsx index dc3ccc8b9..40203ae6a 100644 --- a/src/lib/api-depot.tsx +++ b/src/lib/api-depot.tsx @@ -2,14 +2,15 @@ import { customFetch } from '@/lib/fetch' import { Habilitation, Revision } from '@/types/api-depot.types' import { getDataset } from './api-data-gouv' import { BANCommune } from '@/types/api-ban.types' +import { env } from 'next-runtime-env' -if (!process.env.NEXT_PUBLIC_API_DEPOT_URL) { +if (!env('NEXT_PUBLIC_API_DEPOT_URL')) { throw new Error('NEXT_PUBLIC_API_DEPOT_URL is not defined') } export async function getHabilitation(habilitationId: string, opts = { useProxy: true }): Promise { - const baseUrl = opts.useProxy ? '/api/proxy-api-depot' : `${process.env.NEXT_PUBLIC_API_DEPOT_URL}` - const fetchOptions = opts.useProxy ? {} : { headers: { Authorization: `Token ${process.env.API_DEPOT_TOKEN}` } } + const baseUrl = opts.useProxy ? '/api/proxy-api-depot' : `${env('NEXT_PUBLIC_API_DEPOT_URL')}` + const fetchOptions = opts.useProxy ? {} : { headers: { Authorization: `Token ${env('API_DEPOT_TOKEN')}` } } return customFetch(`${baseUrl}/habilitations/${habilitationId}`, fetchOptions) } @@ -33,15 +34,15 @@ export function validateHabilitationPinCode(habilitationId: string, code: string } export async function getRevision(revisionId: string): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_API_DEPOT_URL}/revisions/${revisionId}`) + return customFetch(`${env('NEXT_PUBLIC_API_DEPOT_URL')}/revisions/${revisionId}`) } export async function getCurrentRevision(codeCommune: string): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_API_DEPOT_URL}/communes/${codeCommune}/current-revision`) + return customFetch(`${env('NEXT_PUBLIC_API_DEPOT_URL')}/communes/${codeCommune}/current-revision`) } export async function getRevisions(codeCommune: string): Promise { - const revisions = await customFetch(`${process.env.NEXT_PUBLIC_API_DEPOT_URL}/communes/${codeCommune}/revisions`) + const revisions = await customFetch(`${env('NEXT_PUBLIC_API_DEPOT_URL')}/communes/${codeCommune}/revisions`) return revisions.sort((a: Revision, b: Revision) => { return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() @@ -81,7 +82,7 @@ export function publishRevision(revisionId: string, body: string) { } export const getRevisionDownloadUrl = (revisionId: string) => { - return `${process.env.NEXT_PUBLIC_API_DEPOT_URL}/revisions/${revisionId}/files/bal/download` + return `${env('NEXT_PUBLIC_API_DEPOT_URL')}/revisions/${revisionId}/files/bal/download` } const frDateFormatter = new Intl.DateTimeFormat('fr', { day: 'numeric', month: 'long', year: 'numeric' }) diff --git a/src/lib/api-etablissement-public.ts b/src/lib/api-etablissement-public.ts index 50c2f7dbf..f1de975ae 100644 --- a/src/lib/api-etablissement-public.ts +++ b/src/lib/api-etablissement-public.ts @@ -1,7 +1,7 @@ import { APIEtablissementPublicMairie } from '@/types/api-etablissement-public.types' import { customFetch } from './fetch' - -if (!process.env.NEXT_PUBLIC_API_ETABLISSEMENTS_PUBLIC) { +import { env } from 'next-runtime-env' +if (!env('NEXT_PUBLIC_API_ETABLISSEMENTS_PUBLIC')) { throw new Error('NEXT_PUBLIC_API_ETABLISSEMENTS_PUBLIC is not defined') } @@ -9,7 +9,7 @@ export async function getMairiePageURL(codeCommune: string) { // Documentation : https://api-lannuaire.service-public.fr/explore/dataset/api-lannuaire-administration/information/ const route = 'catalog/datasets/api-lannuaire-administration/records' const query = `select=nom,url_service_public&where=code_insee_commune="${codeCommune}" and pivot LIKE "mairie"` - const url = `${process.env.NEXT_PUBLIC_API_ETABLISSEMENTS_PUBLIC}/${route}?${query}` + const url = `${env('NEXT_PUBLIC_API_ETABLISSEMENTS_PUBLIC')}/${route}?${query}` const response: { results: APIEtablissementPublicMairie[] } = await customFetch(url) if (!response?.results || response.results.length === 0) { @@ -84,7 +84,7 @@ export async function getMairie(codeCommune: string): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_API_GEO_URL}/departements`) + return customFetch(`${env('NEXT_PUBLIC_API_GEO_URL')}/departements`) } export function isCodeDepNaive(token: string) { @@ -18,11 +19,11 @@ export function isCodeDepNaive(token: string) { } export function getCommune(code: string): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_API_GEO_URL}/communes/${code}`) + return customFetch(`${env('NEXT_PUBLIC_API_GEO_URL')}/communes/${code}`) } export function getEPCI(code: string, fields?: string[]): Promise { - const url = new URL(`${process.env.NEXT_PUBLIC_API_GEO_URL}/epcis/${code}`) + const url = new URL(`${env('NEXT_PUBLIC_API_GEO_URL')}/epcis/${code}`) if (fields) { url.searchParams.append('fields', fields.toString()) @@ -34,7 +35,7 @@ export function getEPCI(code: string, fields?: string[]): Promise { export function getCommunes(args: any): Promise { const { q, fields, limit, boost, type } = args const code = q.match(/^\d{5}$/) ? q : undefined - let url = code ? `${process.env.NEXT_PUBLIC_API_GEO_URL}/communes?code=${code}` : `${process.env.NEXT_PUBLIC_API_GEO_URL}/communes?nom=${encodeURIComponent(q)}` + let url = code ? `${env('NEXT_PUBLIC_API_GEO_URL')}/communes?code=${code}` : `${env('NEXT_PUBLIC_API_GEO_URL')}/communes?nom=${encodeURIComponent(q)}` if (fields) { url += `&fields=${fields.join(',')}` @@ -56,7 +57,7 @@ export function getCommunes(args: any): Promise { } export function getEpcis({ q, limit, fields }: { q: string, limit: number, fields: string[] }): Promise { - const url = new URL(`${process.env.NEXT_PUBLIC_API_GEO_URL}/epcis`) + const url = new URL(`${env('NEXT_PUBLIC_API_GEO_URL')}/epcis`) if (q) { url.searchParams.append('nom', q) @@ -74,9 +75,9 @@ export function getEpcis({ q, limit, fields }: { q: string, limit: number, field } export function getEpciCommunes(code: string): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_API_GEO_URL}/epcis/${code}/communes`) + return customFetch(`${env('NEXT_PUBLIC_API_GEO_URL')}/epcis/${code}/communes`) } export function getDepartementCommunes(code: string): Promise { - return customFetch(`${process.env.NEXT_PUBLIC_API_GEO_URL}/departements/${code}/communes`) + return customFetch(`${env('NEXT_PUBLIC_API_GEO_URL')}/departements/${code}/communes`) } diff --git a/src/lib/api-mes-adresses.ts b/src/lib/api-mes-adresses.ts index 2d697f58b..8b5d7080a 100644 --- a/src/lib/api-mes-adresses.ts +++ b/src/lib/api-mes-adresses.ts @@ -1,12 +1,13 @@ import { BaseAdresseLocale } from '@/types/api-mes-adresses.types' import { customFetch } from './fetch' +import { env } from 'next-runtime-env' -if (!process.env.NEXT_PUBLIC_BAL_API_URL) { +if (!env('NEXT_PUBLIC_BAL_API_URL')) { throw new Error('NEXT_PUBLIC_BAL_API_URL is not defined') } export async function getStatsBals(fields: string[], codeCommunes: string[]): Promise[]> { - const url = new URL(`${process.env.NEXT_PUBLIC_BAL_API_URL}/stats/bals`) + const url = new URL(`${env('NEXT_PUBLIC_BAL_API_URL')}/stats/bals`) for (const field of fields) { url.searchParams.append('fields', field) } @@ -21,5 +22,5 @@ export async function getStatsBals(fields: string[], codeCommunes: string[]): Pr } export async function getBalsStatus() { - return customFetch(`${process.env.NEXT_PUBLIC_BAL_API_URL}/stats/bals/status`) + return customFetch(`${env('NEXT_PUBLIC_BAL_API_URL')}/stats/bals/status`) } diff --git a/src/lib/api-moissonneur-bal.ts b/src/lib/api-moissonneur-bal.ts index c0ff66f0d..f2e61b86f 100644 --- a/src/lib/api-moissonneur-bal.ts +++ b/src/lib/api-moissonneur-bal.ts @@ -1,24 +1,25 @@ import { customFetch } from '@/lib/fetch' import { ExtendedSourceMoissoneurType, HarvestMoissonneurType, OrganizationMoissoneurType, RevisionMoissoneurType } from '@/types/api-moissonneur-bal.types' +import { env } from 'next-runtime-env' -if (!process.env.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL) { +if (!env('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL')) { throw new Error('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL is not defined in the environment') } export async function getOrganization(id: string): Promise { - const url = new URL(`${process.env.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL}/organizations/${id}`) + const url = new URL(`${env('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL')}/organizations/${id}`) return customFetch(url) } export async function getOrganizationSources(id: string): Promise { - const url = new URL(`${process.env.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL}/organizations/${id}/sources`) + const url = new URL(`${env('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL')}/organizations/${id}/sources`) return customFetch(url) } export async function getSourceHarvests(id: string, page = 1, limit = 20): Promise<{ results: HarvestMoissonneurType[], count: number }> { - const url = `${process.env.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL}/sources/${id}/harvests?` + new URLSearchParams({ + const url = `${env('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL')}/sources/${id}/harvests?` + new URLSearchParams({ limit: String(limit), offset: String(limit * (page - 1)), }) @@ -27,11 +28,11 @@ export async function getSourceHarvests(id: string, page = 1, limit = 20): Promi } export async function getSourceRevisions(id: string): Promise { - const url = new URL(`${process.env.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL}/sources/${id}/last-updated-revisions`) + const url = new URL(`${env('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL')}/sources/${id}/last-updated-revisions`) return customFetch(url) } export function getFileLink(id: string) { - return `${process.env.NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL}/files/${id}/download` + return `${env('NEXT_PUBLIC_MOISSONNEUR_BAL_API_URL')}/files/${id}/download` } diff --git a/src/lib/api-wikidata.ts b/src/lib/api-wikidata.ts index ebb9d7384..9087149e5 100644 --- a/src/lib/api-wikidata.ts +++ b/src/lib/api-wikidata.ts @@ -43,3 +43,32 @@ export const getCommuneFlag = async (codeCommune: string): Promise ` + SELECT ?city + ?inseeCode + ?cityLabel + ?logo + WHERE + { + ?city wdt:P374 "${codeCommune}". + ?city wdt:P374 ?inseeCode. + OPTIONAL { ?city wdt:P154 ?logo. } # Récupère le logo de la ville si disponible + SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } + } + ORDER BY ?inseeCode +` + +export const getCommuneLogo = async (codeCommune: string): Promise => { + const url = `${BASE_URL}/sparql?query=${encodeURIComponent(communeLogoQuery(codeCommune))}` + // Wikidata blocks requests without a user agent specific enough + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + }, + }) + const responseBody = await response.text() + const flagUrl = responseBody.match(/http.*\.png/)?.[0] + + return flagUrl +} diff --git a/src/lib/blog.ts b/src/lib/blog.ts index 5f19f85c8..a84d1ef5e 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -1,7 +1,7 @@ -import getConfig from 'next/config' +import { env } from 'next-runtime-env' -const URL = process.env.NEXT_PUBLIC_GHOST_URL -const KEY = process.env.NEXT_PUBLIC_GHOST_KEY +const URL = env ('NEXT_PUBLIC_GHOST_URL') +const KEY = env ('NEXT_PUBLIC_GHOST_KEY') const LIMIT = 15 const INCLUDE = 'authors,tags' diff --git a/src/lib/data-service.ts b/src/lib/data-service.ts new file mode 100644 index 000000000..ba998f65f --- /dev/null +++ b/src/lib/data-service.ts @@ -0,0 +1,131 @@ +import { S3 } from '@aws-sdk/client-s3' +import { GetServerSidePropsContext } from 'next' +import { dataConfig } from '@/views/data/config' +import { + getAlias, + getFormatedDate, + asyncSendS3, + listObjectsRecursively, + autorizedPathS3, +} from '@/views/data/utils' +import { env } from 'next-runtime-env' + +const S3_CONFIG_ACCESS_KEY_ID = env('S3_CONFIG_ACCESS_KEY_ID') +const S3_CONFIG_SECRET_ACCESS_KEY = env('S3_CONFIG_SECRET_ACCESS_KEY') +const S3_CONFIG_REGION = env('S3_CONFIG_REGION') +const S3_CONFIG_ENDPOINT = env('S3_CONFIG_ENDPOINT') + +export const bucketName = 'prd-ign-mut-ban' +export const rootDir = ['adresse-data'] + +const clientS3 = new S3({ + credentials: { + accessKeyId: S3_CONFIG_ACCESS_KEY_ID || 'default-access-key-id', + secretAccessKey: S3_CONFIG_SECRET_ACCESS_KEY || 'default-secret-key', + }, + region: S3_CONFIG_REGION || 'default-region', + endpoint: S3_CONFIG_ENDPOINT || '', +}) + +interface Context extends GetServerSidePropsContext { + params: { + path: string[] + } +} + +export async function handleS3Data(context: Context) { + const { params, res, req } = context + const { path: paramPathRaw = [] } = params + const config = dataConfig?.directory.find(({ path }) => path === paramPathRaw.join('/')) || {} + const alias = await getAlias(clientS3, bucketName)(rootDir, dataConfig?.alias, paramPathRaw.join('/') || '') + + const paramPath = alias && (new RegExp(`^${alias.parent}/${alias.name}`)).test(paramPathRaw.join('/')) + ? paramPathRaw.join('/').replace(new RegExp(`^${alias.parent}/${alias.name}`), `${alias.parent}/${alias.target}`).split('/') + : paramPathRaw + const dirPath = `${paramPath.join('/')}` + const formattedDate = getFormatedDate() + const s3ObjectPath = [...rootDir, ...paramPath].join('/') + + try { + const s3Head = await clientS3.headObject({ + Bucket: bucketName, + Key: s3ObjectPath, + }) + + try { + await asyncSendS3(clientS3)((req as unknown as Request), res, { + params: { + ...(req?.headers?.range ? { Range: req.headers.range } : {}), + Bucket: bucketName, + Key: s3ObjectPath, + }, + fileName: paramPath[paramPath.length - 1], + metadata: s3Head, + }) + } + catch (err) { + console.warn(`[${formattedDate} - ERROR]`, 'File access error:', err) + return { + props: { + errorCode: 502, + errorMessage: 'Une erreur est survenue lors de la génération du téléchargement.', + }, + } + } + } + catch { + const s3DirPath = `${s3ObjectPath}/` + const s3Objects = await listObjectsRecursively(clientS3, bucketName)(s3DirPath) + if (s3Objects) { + const s3data = [ + ...(s3Objects || []) + .filter(({ name }) => autorizedPathS3(name).auth) + .map(({ name, path, isDirectory, fileInfo, ...rest }) => ({ + name, + path: alias && alias.parent !== dirPath + ? path.replace( + (new RegExp(`^${rootDir.join('/')}/${alias.parent}/${alias.target}`)), + `${rootDir.join('/')}/${alias.parent}/${alias.name}` + ) + : path, + isDirectory, + ...(fileInfo ? { fileInfo } : {}), + ...rest, + })), + ] + const s3contentDir = [ + ...s3data, + ...(alias && alias.parent === dirPath + ? [{ + ...alias, + ...(s3data.find(({ name }) => name === alias.target) || {}), + name: alias.name, + path: `${s3ObjectPath}/${alias.name}`, + }] + : []), + ].sort( + (a, b) => ( + a.name.localeCompare(b.name) + || Number(a.isDirectory) - Number(b.isDirectory) + ) + ) + + return { + props: { + title: ['data', ...paramPath].join('/') || '', + path: paramPathRaw || [], + data: s3contentDir || [], + config, + }, + } + } + + console.warn(`[${formattedDate} - ERROR]`, 'S3 Object - Try access to unknown object:', s3DirPath) + res.statusCode = 404 + return { + props: { errorCode: 404 }, + } + } + + return { props: {} } +} diff --git a/src/lib/deploiement-stats.ts b/src/lib/deploiement-stats.ts index 19a688c10..646da7e71 100644 --- a/src/lib/deploiement-stats.ts +++ b/src/lib/deploiement-stats.ts @@ -1,17 +1,18 @@ import { customFetch } from './fetch' import { keyBy, groupBy } from 'lodash' import { getContourCommune } from '../utils/contours-communes' -import { DeploiementBALSearchResult } from '@/app/deploiement-bal/page' +import { DeploiementBALSearchResult } from '@/hooks/useStatsDeploiement' +import { env } from 'next-runtime-env' -if (!process.env.NEXT_PUBLIC_API_BAN_URL) { +if (!env('NEXT_PUBLIC_API_BAN_URL')) { throw new Error('NEXT_PUBLIC_API_BAN_URL is not defined in the environment') } -if (!process.env.NEXT_PUBLIC_API_DEPOT_URL) { +if (!env('NEXT_PUBLIC_API_DEPOT_URL')) { throw new Error('NEXT_PUBLIC_API_DEPOT_URL is not defined in the environment') } -if (!process.env.NEXT_PUBLIC_BAL_API_URL) { +if (!env('NEXT_PUBLIC_BAL_API_URL')) { throw new Error('NEXT_PUBLIC_BAL_API_URL is not defined in the environment') } @@ -59,9 +60,9 @@ type CommuneSummary = { } export async function fetchStatsData() { - const currentRevisions: RevisionSummary[] = await customFetch(`${process.env.NEXT_PUBLIC_API_DEPOT_URL}/current-revisions`) - const communesSummary: CommuneSummary[] = await customFetch(`${process.env.NEXT_PUBLIC_API_BAN_URL}/api/communes-summary`) - const bals = await customFetch(`${process.env.NEXT_PUBLIC_BAL_API_URL}/stats/bals?fields=id&fields=commune&fields=status`, { method: 'POST' }) + const currentRevisions: RevisionSummary[] = await customFetch(`${env('NEXT_PUBLIC_API_DEPOT_URL')}/current-revisions`) + const communesSummary: CommuneSummary[] = await customFetch(`${env('NEXT_PUBLIC_API_BAN_URL')}/api/communes-summary`) + const bals = await customFetch(`${env('NEXT_PUBLIC_BAL_API_URL')}/stats/bals?fields=id&fields=commune&fields=status`, { method: 'POST' }) return { currentRevisions, communesSummary, bals } } @@ -136,4 +137,4 @@ export async function computeStats({ currentRevisions, communesSummary, bals }: } } -export const mapToSearchResult = (values: any[], type: 'EPCI' | 'Département'): DeploiementBALSearchResult[] => values.map(({ code, nom, centre, contour }) => ({ code, type, nom, center: centre, contour })) +export const mapToSearchResult = (values: any[], type: DeploiementBALSearchResult['type']): DeploiementBALSearchResult[] => values.map(({ code, nom, centre, contour }) => ({ code, type, nom, center: centre, contour })) diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index b9d41e611..606302c5d 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -5,9 +5,9 @@ import remarkHeadingId from 'remark-heading-id' import html from 'remark-html' import matter from 'gray-matter' - -const { NEXT_PUBLIC_ADRESSE_URL, NODE_ENV } = process.env - +import { env } from 'next-runtime-env' +const NODE_ENV = env('NODE_ENV') +const NEXT_PUBLIC_ADRESSE_URL = env('NEXT_PUBLIC_ADRESSE_URL') // fix unknown property on matter.GrayMatterFile // https://github.com/jonschlinkert/gray-matter/issues/160 interface GreyMatter extends matter.GrayMatterFile { diff --git a/src/lib/matomo.ts b/src/lib/matomo.ts index e7195e090..09d2d9788 100644 --- a/src/lib/matomo.ts +++ b/src/lib/matomo.ts @@ -1,5 +1,6 @@ import { push as matomoPush } from '@socialgouv/matomo-next' +import { env } from 'next-runtime-env' export const matomoTrackEvent = (category: string, ...args: any[]) => { - matomoPush(['trackEvent', `${process.env.NODE_ENV !== 'production' ? 'DEVMODE - ' : ''}${category}`, ...args]) + matomoPush(['trackEvent', `${env('NODE_ENV') !== 'production' ? 'DEVMODE - ' : ''}${category}`, ...args]) } diff --git a/src/pages/api/proxy-api-depot/[...path].ts b/src/pages/api/proxy-api-depot/[...path].ts index 2913cffff..66e6bcc7b 100644 --- a/src/pages/api/proxy-api-depot/[...path].ts +++ b/src/pages/api/proxy-api-depot/[...path].ts @@ -4,6 +4,7 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { createProxyMiddleware, } from 'http-proxy-middleware' +import { env } from 'next-runtime-env' export const config = { api: { @@ -13,10 +14,10 @@ export const config = { } const proxy = createProxyMiddleware({ - target: process.env.NEXT_PUBLIC_API_DEPOT_URL, + target: env('NEXT_PUBLIC_API_DEPOT_URL'), changeOrigin: true, headers: { - Authorization: `Token ${process.env.API_DEPOT_TOKEN}`, + Authorization: `Token ${env('API_DEPOT_TOKEN')}`, }, pathRewrite: { '^/api/proxy-api-depot': '', diff --git a/src/pages/data/[[...path]].tsx b/src/pages/data/[[...path]].tsx index d5b3ae8dd..ae2b70fe2 100644 --- a/src/pages/data/[[...path]].tsx +++ b/src/pages/data/[[...path]].tsx @@ -1,26 +1,11 @@ import type { GetServerSidePropsContext } from 'next' - -import { S3 } from '@aws-sdk/client-s3' import PropTypes from 'prop-types' import Section from '@/components/Section' import Breadcrumb from '@/layouts/Breadcrumb' import NotFoundPage from '@/app/not-found' -// import sendToTracker, { getDownloadToEventTracker } from '@/lib/util/analytics-tracker' -import { dataConfig, pageConfig } from '@/views/data/config' +import { pageConfig } from '@/views/data/config' import Data from '@/views/data/components/Data' -import { - getAlias, - getFormatedDate, - asyncSendS3, - listObjectsRecursively, - autorizedPathS3, -} from '@/views/data/utils' - -interface Context extends GetServerSidePropsContext { - params: { - path: string[] - } -} +import { handleS3Data } from '@/lib/data-service' interface DataPageProps { title: string @@ -31,129 +16,10 @@ interface DataPageProps { errorMessage: string } -const { - S3_CONFIG_ACCESS_KEY_ID, - S3_CONFIG_SECRET_ACCESS_KEY, - S3_CONFIG_REGION, - S3_CONFIG_ENDPOINT, -} = process.env - const { rootLink } = pageConfig -const bucketName = 'prd-ign-mut-ban' -const rootDir = ['adresse-data'] -const clientS3 = new S3({ - credentials: { - accessKeyId: S3_CONFIG_ACCESS_KEY_ID || '', - secretAccessKey: S3_CONFIG_SECRET_ACCESS_KEY || '', - }, - region: S3_CONFIG_REGION || '', - endpoint: S3_CONFIG_ENDPOINT || '', -}) - -export async function getServerSideProps(context: Context) { - const { params, res, req } = context - const { path: paramPathRaw = [] } = params - const config = dataConfig?.directory.find(({ path }) => path === paramPathRaw.join('/')) || {} - const alias = await getAlias(clientS3, bucketName)(rootDir, dataConfig?.alias, paramPathRaw.join('/') || '') - - const paramPath = alias && (new RegExp(`^${alias.parent}/${alias.name}`)).test(paramPathRaw.join('/')) - ? paramPathRaw.join('/').replace(new RegExp(`^${alias.parent}/${alias.name}`), `${alias.parent}/${alias.target}`).split('/') - : paramPathRaw - const dirPath = `${paramPath.join('/')}` - const formattedDate = getFormatedDate() - const s3ObjectPath = [...rootDir, ...paramPath].join('/') - - try { - const s3Head = await clientS3.headObject({ - Bucket: bucketName, - Key: s3ObjectPath, - }) - - // NO ERROR > PATH IS A FILE - try { - // sendToTracker(getDownloadToEventTracker({ - // downloadDataType: `${paramPath[0]}${req?.headers?.range ? ' (Partial)' : ''}`, - // downloadFileName: dirPath, - // nbDownload: 1 - // })) - await asyncSendS3(clientS3)((req as unknown as Request), res, { - params: { - ...(req?.headers?.range ? { Range: req.headers.range } : {}), - Bucket: bucketName, - Key: s3ObjectPath, - }, - fileName: paramPath[paramPath.length - 1], - metadata: s3Head, - }) - } - catch (err) { - console.warn(`[${formattedDate} - ERROR]`, 'File access error:', err) - return { - props: { - errorCode: 502, - errorMessage: 'Une erreur est survenue lors de la génération du téléchargement.', - }, - } - } - } - catch { - // ERROR > PATH SHOULD BE DIRECTORY - const s3DirPath = `${s3ObjectPath}/` - const s3Objects = await listObjectsRecursively(clientS3, bucketName)(s3DirPath) - if (s3Objects) { - const s3data = [ - ...(s3Objects || []) - .filter(({ name }) => autorizedPathS3(name).auth) - .map(({ name, path, isDirectory, fileInfo, ...rest }) => ({ - name, - path: alias && alias.parent !== dirPath - ? path.replace( - (new RegExp(`^${rootDir.join('/')}/${alias.parent}/${alias.target}`)), - `${rootDir.join('/')}/${alias.parent}/${alias.name}` - ) - : path, - isDirectory, - ...(fileInfo ? { fileInfo } : {}), - ...rest, - })), - ] - const s3contentDir = [ - ...s3data, - ...(alias && alias.parent === dirPath - ? [{ - ...alias, - ...(s3data.find(({ name }) => name === alias.target) || {}), - name: alias.name, - path: `${s3ObjectPath}/${alias.name}`, - }] - : []), - ].sort( - (a, b) => ( - a.name.localeCompare(b.name) - || Number(a.isDirectory) - Number(b.isDirectory) - ) - ) - - return { - props: { - title: ['data', ...paramPath].join('/') || '', - path: paramPathRaw || [], - data: s3contentDir || [], - config, - }, - } - } - - // OBJECT DO NOT EXIST > IS UNKNOWN PATH - console.warn(`[${formattedDate} - ERROR]`, 'S3 Object - Try access to unknown object:', s3DirPath) - res.statusCode = 404 - return { - props: { errorCode: 404 }, - } - } - - return { props: {} } +export async function getServerSideProps(context: GetServerSidePropsContext) { + return handleS3Data(context as any) } export default function DataPage({ title, path, data, config, errorCode, errorMessage }: DataPageProps) { diff --git a/src/utils/events.ts b/src/utils/events.ts index c39b24d73..bd3332c37 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -11,7 +11,7 @@ export function mapEvents(events: EventType[], typeFilter?: string) { const [hourB, minuteB] = b.startHour.split(':') dateB.setHours(parseInt(hourB), parseInt(minuteB)) - return dateB.getTime() - dateA.getTime() + return dateA.getTime() - dateB.getTime() }) if (typeFilter) {