From 2b07b5433b178ecb4e626c0edfa72bd7ced64a07 Mon Sep 17 00:00:00 2001 From: itMatos Date: Thu, 21 May 2026 22:45:21 -0300 Subject: [PATCH 01/13] feat: add Map component and integrate Leaflet for map functionality --- .prettierrc | 15 ++++++++++ .prettierrc.js | 8 ------ next-env.d.ts | 2 +- package-lock.json | 61 ++++++++++++++++++++++++++++++++++++++++ package.json | 4 +++ pages/launches/index.tsx | 12 ++++++++ src/components/Map.tsx | 28 ++++++++++++++++++ 7 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 .prettierrc delete mode 100644 .prettierrc.js create mode 100644 pages/launches/index.tsx create mode 100644 src/components/Map.tsx diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..35606a40 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "printWidth": 130, + "bracketSameLine": true, + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "trailingComma": "none", + "semi": true, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "auto", + "htmlWhitespaceSensitivity": "ignore", + "embeddedLanguageFormatting": "auto", + "angular-eslint/no-injectable-provided-in-root": "off" +} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 879c3d79..00000000 --- a/.prettierrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - trailingComma: 'all', - singleQuote: true, - bracketSpacing: 'all', - jsxBracketSameLine: true, - printWidth: 120, - tabWidth: 4, -}; diff --git a/next-env.d.ts b/next-env.d.ts index 19709046..7996d352 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/package-lock.json b/package-lock.json index af23976b..aca4dc4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "license": "MIT", "dependencies": { "hamburger-react": "^2.4.0", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", "next": "^16.2.6", "next-seo": "^4.24.0", "next-translate": "^1.0.6", @@ -18,12 +20,14 @@ "react-countup": "^6.1.1", "react-dom": "^19.2.6", "react-icons": "^4.2.0", + "react-leaflet": "^5.0.0", "react-player": "^2.9.0", "react-responsive-carousel": "^3.2.22", "react-rotating-text": "^1.4.1", "react-visibility-sensor": "^5.1.1" }, "devDependencies": { + "@types/leaflet": "^1.9.21", "@types/node": "^25.9.1", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", @@ -37,6 +41,9 @@ "eslint-plugin-react-hooks": "^4.2.0", "prettier": "^2.3.0", "typescript": "^6.0.3" + }, + "engines": { + "node": ">=20.9.0" } }, "node_modules/@emnapi/runtime": { @@ -788,6 +795,17 @@ "node": ">= 8" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -804,6 +822,13 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -811,6 +836,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "25.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", @@ -3141,6 +3176,18 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/leaflet-defaulticon-compatibility": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.2.tgz", + "integrity": "sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3813,6 +3860,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-player": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.1.tgz", diff --git a/package.json b/package.json index 2f8416f9..64c835ba 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "homepage": "https://github.com/zenitheesc/zenith-website#readme", "dependencies": { "hamburger-react": "^2.4.0", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", "next": "^16.2.6", "next-seo": "^4.24.0", "next-translate": "^1.0.6", @@ -60,12 +62,14 @@ "react-countup": "^6.1.1", "react-dom": "^19.2.6", "react-icons": "^4.2.0", + "react-leaflet": "^5.0.0", "react-player": "^2.9.0", "react-responsive-carousel": "^3.2.22", "react-rotating-text": "^1.4.1", "react-visibility-sensor": "^5.1.1" }, "devDependencies": { + "@types/leaflet": "^1.9.21", "@types/node": "^25.9.1", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx new file mode 100644 index 00000000..f4adce89 --- /dev/null +++ b/pages/launches/index.tsx @@ -0,0 +1,12 @@ +import dynamic from 'next/dynamic'; +import { useMemo } from 'react'; + +export default function LaunchesPage() { + const Map = useMemo(() => dynamic(() => import('../../src/components/Map'), { ssr: false }), []); + return ( +
+ teste + +
+ ); +} diff --git a/src/components/Map.tsx b/src/components/Map.tsx new file mode 100644 index 00000000..0ded06dd --- /dev/null +++ b/src/components/Map.tsx @@ -0,0 +1,28 @@ +// src/components/Map.tsx +import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; +import 'leaflet-defaulticon-compatibility'; +import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'; + +type MapProps = { + position?: [number, number]; + zoom?: number; +}; + +export default function MyMap(props: MapProps) { + const { position = [0, 0], zoom = 2 } = props; + + return ( + + + + + A pretty CSS3 popup.
Easily customizable. +
+
+
+ ); +} From 39a133b2d45b470a9cf1cebe4beee57d4b269a57 Mon Sep 17 00:00:00 2001 From: itMatos Date: Thu, 21 May 2026 22:56:07 -0300 Subject: [PATCH 02/13] feat: refactor Map component and add MapProps type definition --- pages/launches/index.tsx | 7 +------ src/components/Map.tsx | 30 ++++++++++++++---------------- src/types/map.types.ts | 4 ++++ 3 files changed, 19 insertions(+), 22 deletions(-) create mode 100644 src/types/map.types.ts diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index f4adce89..b2052722 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -3,10 +3,5 @@ import { useMemo } from 'react'; export default function LaunchesPage() { const Map = useMemo(() => dynamic(() => import('../../src/components/Map'), { ssr: false }), []); - return ( -
- teste - -
- ); + return ; } diff --git a/src/components/Map.tsx b/src/components/Map.tsx index 0ded06dd..b3d3dedf 100644 --- a/src/components/Map.tsx +++ b/src/components/Map.tsx @@ -3,26 +3,24 @@ import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import 'leaflet-defaulticon-compatibility'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'; - -type MapProps = { - position?: [number, number]; - zoom?: number; -}; +import { MapProps } from '../types/map.types'; export default function MyMap(props: MapProps) { const { position = [0, 0], zoom = 2 } = props; return ( - - - - - A pretty CSS3 popup.
Easily customizable. -
-
-
+
+ + + + + A pretty CSS3 popup.
Easily customizable. +
+
+
+
); } diff --git a/src/types/map.types.ts b/src/types/map.types.ts new file mode 100644 index 00000000..9046807a --- /dev/null +++ b/src/types/map.types.ts @@ -0,0 +1,4 @@ +export type MapProps = { + position?: [number, number]; + zoom?: number; +}; From 50ef3dc60abd40aa01f70b91ccaddbe37f86166b Mon Sep 17 00:00:00 2001 From: itMatos Date: Fri, 22 May 2026 14:19:43 -0300 Subject: [PATCH 03/13] feat: implement launches page with map integration and API service for fetching launches data --- pages/launches/index.tsx | 2 +- src/components/Map/Map.styles.ts | 0 src/components/{ => Map}/Map.tsx | 13 +++++-- src/core/api/launches/launches-api.service.ts | 20 +++++++++++ src/core/services/launches.service.ts | 0 src/environments/environment.ts | 13 +++++++ src/types/api/launches-api.types.ts | 36 +++++++++++++++++++ tsconfig.json | 20 +++-------- 8 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 src/components/Map/Map.styles.ts rename src/components/{ => Map}/Map.tsx (72%) create mode 100644 src/core/api/launches/launches-api.service.ts create mode 100644 src/core/services/launches.service.ts create mode 100644 src/environments/environment.ts create mode 100644 src/types/api/launches-api.types.ts diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index b2052722..4cf2ed99 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -2,6 +2,6 @@ import dynamic from 'next/dynamic'; import { useMemo } from 'react'; export default function LaunchesPage() { - const Map = useMemo(() => dynamic(() => import('../../src/components/Map'), { ssr: false }), []); + const Map = useMemo(() => dynamic(() => import('../../src/components/Map/Map'), { ssr: false }), []); return ; } diff --git a/src/components/Map/Map.styles.ts b/src/components/Map/Map.styles.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Map.tsx b/src/components/Map/Map.tsx similarity index 72% rename from src/components/Map.tsx rename to src/components/Map/Map.tsx index b3d3dedf..e1d95257 100644 --- a/src/components/Map.tsx +++ b/src/components/Map/Map.tsx @@ -1,12 +1,21 @@ -// src/components/Map.tsx import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import 'leaflet-defaulticon-compatibility'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'; -import { MapProps } from '../types/map.types'; +import { MapProps } from '@/src/types/map.types'; +import { useEffect } from 'react'; +import { getAllLaunches } from '@/src/core/api/launches/launches-api.service'; export default function MyMap(props: MapProps) { const { position = [0, 0], zoom = 2 } = props; + useEffect(() => { + try { + const response = getAllLaunches(); + console.log(response); + } catch (error) { + console.error('Error fetching launches data:', error); + } + }, []); return (
diff --git a/src/core/api/launches/launches-api.service.ts b/src/core/api/launches/launches-api.service.ts new file mode 100644 index 00000000..c49dcb51 --- /dev/null +++ b/src/core/api/launches/launches-api.service.ts @@ -0,0 +1,20 @@ +import { environment } from '@/src/environments/environment'; +import { LaunchRecord, LaunchSummary } from '@/src/types/api/launches-api.types'; + +export const getAllLaunches = async (): Promise => { + const response = await fetch(environment.launchesEndpoints.launches.all); + if (!response.ok) { + throw new Error('Failed to fetch launches data'); + } + const data: LaunchSummary[] = await response.json(); + return data; +}; + +export const getLaunchContent = async (downloadUrl: string): Promise => { + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error('Failed to fetch launch content'); + } + const content = await response.json(); + return content; +}; diff --git a/src/core/services/launches.service.ts b/src/core/services/launches.service.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 00000000..0e080c3f --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,13 @@ +const launchesApiUrl = 'https://zenitheesc.github.io/launches-data' as const; + +const launchesEndpoints = { + launches: { + all: `${launchesApiUrl}/index.json`, + contentLaunch: `${launchesApiUrl}/contents` + } +} as const; + +export const environment = { + launchesApiUrl, + launchesEndpoints +}; diff --git a/src/types/api/launches-api.types.ts b/src/types/api/launches-api.types.ts new file mode 100644 index 00000000..3fdb7df0 --- /dev/null +++ b/src/types/api/launches-api.types.ts @@ -0,0 +1,36 @@ +export type LaunchSummary = { + name: string; + download_url: string; + launch_city: string; + landing_city: string; + max_altitude: number; + launch_datetime: string; +}; + +export type LaunchRecord = { + alt: number; + batt: number; + datetime: string; + frame: number; + frequency: number; + heading: number; + lat: number; + lon: number; + manufacturer: string; + position: string; + sats: number; + serial: string; + software_name: string; + software_version: string; + subtype: string; + time_received: string; + type: string; + upload_time_delta: number; + uploader_alt: number; + uploader_antenna: string; + uploader_callsign: string; + uploader_position: string; + 'user-agent': string; + vel_h: number; + vel_v: number; +}; diff --git a/tsconfig.json b/tsconfig.json index 4785debd..49597a1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es6", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -21,17 +17,9 @@ "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], + "exclude": ["node_modules"] } From 945923141837c8a3037939d9100aca4fb6e2f524 Mon Sep 17 00:00:00 2001 From: itMatos Date: Fri, 22 May 2026 14:36:40 -0300 Subject: [PATCH 04/13] feat: refactor launches API service and add custom hooks for fetching launches data --- src/core/api/launches/launches-api.service.ts | 10 ++----- .../launches/useGetAllLaunches.service.ts | 28 +++++++++++++++++++ .../useGetLaunchContent.service.ts} | 0 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 src/core/services/launches/useGetAllLaunches.service.ts rename src/core/services/{launches.service.ts => launches/useGetLaunchContent.service.ts} (100%) diff --git a/src/core/api/launches/launches-api.service.ts b/src/core/api/launches/launches-api.service.ts index c49dcb51..6f94f155 100644 --- a/src/core/api/launches/launches-api.service.ts +++ b/src/core/api/launches/launches-api.service.ts @@ -1,13 +1,9 @@ import { environment } from '@/src/environments/environment'; import { LaunchRecord, LaunchSummary } from '@/src/types/api/launches-api.types'; -export const getAllLaunches = async (): Promise => { - const response = await fetch(environment.launchesEndpoints.launches.all); - if (!response.ok) { - throw new Error('Failed to fetch launches data'); - } - const data: LaunchSummary[] = await response.json(); - return data; +export const getAllLaunches = async () => { + const allLaunches = await fetch(environment.launchesEndpoints.launches.all); + return allLaunches; }; export const getLaunchContent = async (downloadUrl: string): Promise => { diff --git a/src/core/services/launches/useGetAllLaunches.service.ts b/src/core/services/launches/useGetAllLaunches.service.ts new file mode 100644 index 00000000..bd8329c6 --- /dev/null +++ b/src/core/services/launches/useGetAllLaunches.service.ts @@ -0,0 +1,28 @@ +import { environment } from '@/src/environments/environment'; +import { LaunchRecord } from '@/src/types/api/launches-api.types'; +import { useState } from 'react'; + +export const useGetAllLaunches = () => { + const [content, setContent] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchAllLaunches = async () => { + setIsLoading(true); + setError(null); + try { + const response = await fetch(environment.launchesEndpoints.launches.all); + if (!response.ok) { + throw new Error('Failed to fetch all launches'); + } + const data = await response.json(); + setContent(data); + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + return { content, isLoading, error, fetchAllLaunches }; +}; diff --git a/src/core/services/launches.service.ts b/src/core/services/launches/useGetLaunchContent.service.ts similarity index 100% rename from src/core/services/launches.service.ts rename to src/core/services/launches/useGetLaunchContent.service.ts From e3d181e1edc97152d8cd04e78c07ec7e15e72584 Mon Sep 17 00:00:00 2001 From: itMatos Date: Fri, 22 May 2026 15:04:35 -0300 Subject: [PATCH 05/13] feat: refactor launches data fetching to use custom hook and improve error handling --- src/components/Map/Map.tsx | 13 ++-- src/core/api/launches/launches-api.service.ts | 4 +- .../launches/useGetAllLaunches.service.ts | 62 ++++++++++++------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index e1d95257..ba579577 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -4,18 +4,17 @@ import 'leaflet-defaulticon-compatibility'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'; import { MapProps } from '@/src/types/map.types'; import { useEffect } from 'react'; -import { getAllLaunches } from '@/src/core/api/launches/launches-api.service'; +import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; export default function MyMap(props: MapProps) { const { position = [0, 0], zoom = 2 } = props; + const { launches, isLoadingAllLaunches } = useAllLaunches(); + useEffect(() => { - try { - const response = getAllLaunches(); - console.log(response); - } catch (error) { - console.error('Error fetching launches data:', error); + if (!isLoadingAllLaunches) { + console.log('Lançamentos carregados:', launches); } - }, []); + }, [isLoadingAllLaunches, launches]); return (
diff --git a/src/core/api/launches/launches-api.service.ts b/src/core/api/launches/launches-api.service.ts index 6f94f155..246ec801 100644 --- a/src/core/api/launches/launches-api.service.ts +++ b/src/core/api/launches/launches-api.service.ts @@ -1,8 +1,8 @@ import { environment } from '@/src/environments/environment'; import { LaunchRecord, LaunchSummary } from '@/src/types/api/launches-api.types'; -export const getAllLaunches = async () => { - const allLaunches = await fetch(environment.launchesEndpoints.launches.all); +export const getAllLaunches = async (signal?: AbortSignal) => { + const allLaunches = await fetch(environment.launchesEndpoints.launches.all, { signal }); return allLaunches; }; diff --git a/src/core/services/launches/useGetAllLaunches.service.ts b/src/core/services/launches/useGetAllLaunches.service.ts index bd8329c6..dee83309 100644 --- a/src/core/services/launches/useGetAllLaunches.service.ts +++ b/src/core/services/launches/useGetAllLaunches.service.ts @@ -1,28 +1,44 @@ -import { environment } from '@/src/environments/environment'; -import { LaunchRecord } from '@/src/types/api/launches-api.types'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { LaunchSummary } from '@/src/types/api/launches-api.types'; +import { getAllLaunches } from '../../api/launches/launches-api.service'; -export const useGetAllLaunches = () => { - const [content, setContent] = useState(null); - const [isLoading, setIsLoading] = useState(false); +export const useAllLaunches = () => { + const [launches, setLaunches] = useState([]); + const [isLoadingAllLaunches, setIsLoadingAllLaunches] = useState(true); const [error, setError] = useState(null); - const fetchAllLaunches = async () => { - setIsLoading(true); - setError(null); - try { - const response = await fetch(environment.launchesEndpoints.launches.all); - if (!response.ok) { - throw new Error('Failed to fetch all launches'); + useEffect(() => { + const controller = new AbortController(); + + const fetchLaunches = async () => { + setIsLoadingAllLaunches(true); + setError(null); + + try { + const res = await getAllLaunches(controller.signal); + if (!res.ok) throw new Error('Erro ao buscar lançamentos'); + + const rawData = await res.json(); + setLaunches(rawData); + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + return; + } + + setError(err instanceof Error ? err.message : 'Ocorreu um erro inesperado'); + } finally { + if (!controller.signal.aborted) { + setIsLoadingAllLaunches(false); + } } - const data = await response.json(); - setContent(data); - } catch (err) { - setError(err.message); - } finally { - setIsLoading(false); - } - }; - - return { content, isLoading, error, fetchAllLaunches }; + }; + + fetchLaunches(); + + return () => { + controller.abort(); + }; + }, []); + + return { launches, isLoadingAllLaunches, error }; }; From 1bbfcc781cdd51a0cb27deb73978d59e009b2bb9 Mon Sep 17 00:00:00 2001 From: itMatos Date: Fri, 22 May 2026 16:07:28 -0300 Subject: [PATCH 06/13] feat: enhance launches page with MUI components and loading states - Added MUI dependencies for UI components. - Refactored LaunchesPage to utilize MUI for layout and styling. - Implemented loading and error handling states for fetching launches. - Created a responsive grid layout for displaying launch cards. - Updated API service to handle fetch errors gracefully. - Moved types to shared directory for better organization. - Introduced utility functions for formatting launch names and dates. - Removed unused Map component logic from LaunchesPage. - Created a new service for handling launches API interactions. --- package-lock.json | 797 +++++++++++++++++- package.json | 4 + pages/launches/index.tsx | 96 ++- src/components/LaunchCard/LaunchCard.tsx | 0 src/components/Map/Map.tsx | 9 +- src/core/api/launches/launches-api.service.ts | 13 +- src/core/services/launches.service.ts | 13 + .../launches/useGetAllLaunches.service.ts | 13 +- .../types/api/launches-api.types.ts | 0 src/{ => shared}/types/map.types.ts | 0 src/shared/utils/formatters.utils.ts | 21 + 11 files changed, 928 insertions(+), 38 deletions(-) create mode 100644 src/components/LaunchCard/LaunchCard.tsx create mode 100644 src/core/services/launches.service.ts rename src/{ => shared}/types/api/launches-api.types.ts (100%) rename src/{ => shared}/types/map.types.ts (100%) create mode 100644 src/shared/utils/formatters.utils.ts diff --git a/package-lock.json b/package-lock.json index aca4dc4d..1fe6090c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^9.0.1", + "date-fns": "^4.2.1", "hamburger-react": "^2.4.0", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", @@ -46,6 +50,145 @@ "node": ">=20.9.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -56,6 +199,152 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -623,6 +912,260 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.1.tgz", + "integrity": "sha512-GzamIIhZ1bH77dq7eKaeyRgJdkypsxin4jBFq2EMs4lBWRR0LFO1CSVMsoebn/VvjcNrnrOrjy48MkrkQUK2iw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.1.tgz", + "integrity": "sha512-voyCpeUxcSWLN7KPZuq0pGCIt726T9K6kiVM3XUcywZDAlZSarLHaUxJVQpospbjjOzN53hwyjo8s6KoWl6utw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/core-downloads-tracker": "^9.0.1", + "@mui/system": "^9.0.1", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.4", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.0.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.1.tgz", + "integrity": "sha512-pSIGq4Yw749KHEwlkYZWVERgHgwJELP6ODtBNUfV8V4oIb5H+h7IQDFXuk/b2oQccODK1enJAtiEzlgLZmq+8g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/utils": "^9.0.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz", + "integrity": "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.0.1.tgz", + "integrity": "sha512-WvlioaLxk6ewUIOfh0StxUvOPDS1mCfzaulcudsL1brZNXuh0N9FMk7RpH7ImJKjEz412SEy/V/yvqmtxbqxCQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/private-theming": "^9.0.1", + "@mui/styled-engine": "^9.0.0", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.1", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz", + "integrity": "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.1.tgz", + "integrity": "sha512-f3UO3jNN1pYg5zxqXC81Bvv8hx5ACcYc0387382ZI7M5ono1heIwHYLrKsz85myguWdeVKPRZGmDdynWUBjK2g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/types": "^9.0.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", @@ -795,6 +1338,16 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@react-leaflet/core": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", @@ -856,11 +1409,22 @@ "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -876,6 +1440,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", @@ -1179,6 +1752,42 @@ "node": ">= 0.4" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1263,7 +1872,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1318,6 +1926,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1352,6 +1969,28 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/countup.js": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.10.0.tgz", @@ -1377,7 +2016,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -1441,11 +2079,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.2.1.tgz", + "integrity": "sha512-37RhSdxaG1suen6VDCza6rNrQfooyQh57HFVPwQGEq2QWliVLzPQZ8Oa017weOu+HZCnzI7N3Pf/wyoBKfEqrA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1534,6 +2181,16 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1556,6 +2213,15 @@ "dev": true, "license": "MIT" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -1639,7 +2305,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1737,7 +2402,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2221,6 +2885,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2287,7 +2957,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2573,7 +3242,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2582,6 +3250,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2596,7 +3273,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2671,6 +3347,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -2741,7 +3423,6 @@ "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.3" @@ -3096,6 +3777,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3103,6 +3796,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3202,6 +3901,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/load-script": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", @@ -3292,7 +3997,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3617,7 +4321,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3626,6 +4329,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/particles.js": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/particles.js/-/particles.js-2.0.0.tgz", @@ -3666,9 +4387,17 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3915,6 +4644,22 @@ "react-dom": ">= 15" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-visibility-sensor": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz", @@ -4000,7 +4745,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4335,6 +5079,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4530,6 +5283,12 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4547,7 +5306,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4858,6 +5616,15 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 64c835ba..bb3a64c0 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,10 @@ }, "homepage": "https://github.com/zenitheesc/zenith-website#readme", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^9.0.1", + "date-fns": "^4.2.1", "hamburger-react": "^2.4.0", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index 4cf2ed99..86188d82 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -1,7 +1,95 @@ -import dynamic from 'next/dynamic'; -import { useMemo } from 'react'; +import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; +import { formatLaunchDatetime, formatLaunchName } from '@/src/shared/utils/formatters.utils'; +import { + Alert, + Box, + Button, + Card, + CardActions, + CardContent, + CardHeader, + CircularProgress, + Container, + Stack, + Typography +} from '@mui/material'; +import { useEffect } from 'react'; export default function LaunchesPage() { - const Map = useMemo(() => dynamic(() => import('../../src/components/Map/Map'), { ssr: false }), []); - return ; + const { launches, isLoadingAllLaunches, error } = useAllLaunches(); + + useEffect(() => {}, [isLoadingAllLaunches, launches]); + + return ( + + + + Lançamentos + + + Acompanhe os lançamentos e os dados de cada missão. + + + + {error ? ( + + {error} + + ) : null} + + {isLoadingAllLaunches ? ( + + + + ) : launches.length === 0 ? ( + Nenhum lançamento encontrado. + ) : ( + + {launches.map((launch) => ( + + + {formatLaunchName(launch.name)} + + } + subheader={formatLaunchDatetime(launch.launch_datetime)} + /> + + + + + {launch.launch_city} → {launch.landing_city} + + + + + + Altitude máxima + + + {launch.max_altitude.toLocaleString('pt-BR')} m + + + + + + + + + ))} + + )} + + ); } diff --git a/src/components/LaunchCard/LaunchCard.tsx b/src/components/LaunchCard/LaunchCard.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index ba579577..9d8e93bb 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -2,19 +2,12 @@ import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import 'leaflet-defaulticon-compatibility'; import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'; -import { MapProps } from '@/src/types/map.types'; +import { MapProps } from '@/src/shared/types/map.types'; import { useEffect } from 'react'; import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; export default function MyMap(props: MapProps) { const { position = [0, 0], zoom = 2 } = props; - const { launches, isLoadingAllLaunches } = useAllLaunches(); - - useEffect(() => { - if (!isLoadingAllLaunches) { - console.log('Lançamentos carregados:', launches); - } - }, [isLoadingAllLaunches, launches]); return (
diff --git a/src/core/api/launches/launches-api.service.ts b/src/core/api/launches/launches-api.service.ts index 246ec801..34beed61 100644 --- a/src/core/api/launches/launches-api.service.ts +++ b/src/core/api/launches/launches-api.service.ts @@ -1,9 +1,14 @@ import { environment } from '@/src/environments/environment'; -import { LaunchRecord, LaunchSummary } from '@/src/types/api/launches-api.types'; +import { LaunchRecord, LaunchSummary } from '@/src/shared/types/api/launches-api.types'; -export const getAllLaunches = async (signal?: AbortSignal) => { - const allLaunches = await fetch(environment.launchesEndpoints.launches.all, { signal }); - return allLaunches; +export const getAllLaunches = async (signal?: AbortSignal): Promise => { + const response = await fetch(environment.launchesEndpoints.launches.all, { signal }); + if (!response.ok) { + throw new Error('Failed to fetch launches data'); + } + + const data: LaunchSummary[] = await response.json(); + return data; }; export const getLaunchContent = async (downloadUrl: string): Promise => { diff --git a/src/core/services/launches.service.ts b/src/core/services/launches.service.ts new file mode 100644 index 00000000..d1cb26c9 --- /dev/null +++ b/src/core/services/launches.service.ts @@ -0,0 +1,13 @@ +import { + getAllLaunches as getAllLaunchesApi, + getLaunchContent as getLaunchContentApi +} from '@/src/core/api/launches/launches-api.service'; +import { LaunchRecord, LaunchSummary } from '@/src/shared/types/api/launches-api.types'; + +export const getAllLaunches = async (): Promise => { + return getAllLaunchesApi(); +}; + +export const getLaunchContent = async (downloadUrl: string): Promise => { + return getLaunchContentApi(downloadUrl); +}; diff --git a/src/core/services/launches/useGetAllLaunches.service.ts b/src/core/services/launches/useGetAllLaunches.service.ts index dee83309..9f775417 100644 --- a/src/core/services/launches/useGetAllLaunches.service.ts +++ b/src/core/services/launches/useGetAllLaunches.service.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -import { LaunchSummary } from '@/src/types/api/launches-api.types'; -import { getAllLaunches } from '../../api/launches/launches-api.service'; +import { LaunchSummary } from '@/src/shared/types/api/launches-api.types'; +import { getAllLaunches } from '../launches.service'; export const useAllLaunches = () => { const [launches, setLaunches] = useState([]); @@ -15,11 +15,10 @@ export const useAllLaunches = () => { setError(null); try { - const res = await getAllLaunches(controller.signal); - if (!res.ok) throw new Error('Erro ao buscar lançamentos'); - - const rawData = await res.json(); - setLaunches(rawData); + const rawData = await getAllLaunches(); + if (!controller.signal.aborted) { + setLaunches(rawData); + } } catch (err) { if (err instanceof Error && err.name === 'AbortError') { return; diff --git a/src/types/api/launches-api.types.ts b/src/shared/types/api/launches-api.types.ts similarity index 100% rename from src/types/api/launches-api.types.ts rename to src/shared/types/api/launches-api.types.ts diff --git a/src/types/map.types.ts b/src/shared/types/map.types.ts similarity index 100% rename from src/types/map.types.ts rename to src/shared/types/map.types.ts diff --git a/src/shared/utils/formatters.utils.ts b/src/shared/utils/formatters.utils.ts new file mode 100644 index 00000000..9069ee21 --- /dev/null +++ b/src/shared/utils/formatters.utils.ts @@ -0,0 +1,21 @@ +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +export const formatLaunchName = (name: string): string => { + const parts = name.split(' - '); + const formattedName = parts.length > 1 ? parts[1].trim() : name; + + return formattedName.replace(/\.json$/i, ''); +}; + +export const formatLaunchDatetime = (datetime: string): string => { + const date = new Date(datetime); + const currentYear = new Date().getFullYear(); + const dateYear = date.getFullYear(); + + const formattedDate = format(date, dateYear === currentYear ? 'EEEEEE., dd MMM' : "EEEEEE., dd MMM 'de' yyyy", { + locale: ptBR + }); + + return `${formattedDate} ${date.toLocaleTimeString('pt-BR')}`; +}; From 501f50cd49596052ba50351dfc6f7b7ab3879220 Mon Sep 17 00:00:00 2001 From: itMatos Date: Fri, 22 May 2026 16:11:18 -0300 Subject: [PATCH 07/13] feat: improve error and loading state handling on launches page --- pages/launches/index.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index 86188d82..c989b571 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -15,6 +15,7 @@ import { } from '@mui/material'; import { useEffect } from 'react'; +// TODO mover textos para arquivo de tradução export default function LaunchesPage() { const { launches, isLoadingAllLaunches, error } = useAllLaunches(); @@ -31,19 +32,21 @@ export default function LaunchesPage() { - {error ? ( + {error && ( {error} - ) : null} + )} - {isLoadingAllLaunches ? ( + {isLoadingAllLaunches && ( - ) : launches.length === 0 ? ( - Nenhum lançamento encontrado. - ) : ( + )} + + {!isLoadingAllLaunches && launches.length === 0 && Nenhum lançamento encontrado.} + + {!isLoadingAllLaunches && launches.length > 0 && ( Date: Fri, 22 May 2026 18:12:24 -0300 Subject: [PATCH 08/13] feat: add MUI icons and lab components, enhance launches page with timeline and altitude conversion --- package-lock.json | 72 +++++++++++++++++++++++ package.json | 2 + pages/launches/index.tsx | 86 ++++++++++++++++++---------- src/shared/utils/formatters.utils.ts | 7 ++- 4 files changed, 137 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1fe6090c..78b3bada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.1", + "@mui/lab": "^9.0.0-beta.3", "@mui/material": "^9.0.1", "date-fns": "^4.2.1", "hamburger-react": "^2.4.0", @@ -957,6 +959,76 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.1.tgz", + "integrity": "sha512-5PRpQjVLTNLyV/2J9J53Yz4R0tVbodG0BQDN2zQI1QBG1OPYM25ar+4N20eyFOfJT6zKglLzsnU70+zdVLaTkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^9.0.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/lab": { + "version": "9.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-9.0.0-beta.3.tgz", + "integrity": "sha512-V824ch6JKO14QWcsbzqzyrxQpNKfnbq84NchzEH1e5ry5B4l0n+i8TmJwJWwxjMH0yxyE4tKorywGHu4oKRBlA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@mui/system": "^9.0.1", + "@mui/types": "^9.0.0", + "@mui/utils": "^9.0.1", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^9.0.1", + "@mui/material-pigment-css": "^9.0.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.1.tgz", diff --git a/package.json b/package.json index bb3a64c0..0d57f7a3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^9.0.1", + "@mui/lab": "^9.0.0-beta.3", "@mui/material": "^9.0.1", "date-fns": "^4.2.1", "hamburger-react": "^2.4.0", diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index c989b571..cbbcc6ce 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -1,19 +1,18 @@ -import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; -import { formatLaunchDatetime, formatLaunchName } from '@/src/shared/utils/formatters.utils'; -import { - Alert, - Box, - Button, - Card, - CardActions, - CardContent, - CardHeader, - CircularProgress, - Container, - Stack, - Typography -} from '@mui/material'; import { useEffect } from 'react'; +import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; +import { convertAltitudeToKm, formatLaunchDatetime, formatLaunchName } from '@/src/shared/utils/formatters.utils'; +import { Alert, Box, Button, Card, CardActions, CardContent, CardHeader } from '@mui/material'; +import { Chip, CircularProgress, Container, Stack, Typography } from '@mui/material'; +import { Timeline, TimelineItem, TimelineSeparator, TimelineConnector } from '@mui/lab'; +import { TimelineContent, TimelineDot, timelineItemClasses } from '@mui/lab'; +import type {} from '@mui/lab/themeAugmentation'; +import PinDropIcon from '@mui/icons-material/PinDrop'; +import GpsNotFixedIcon from '@mui/icons-material/GpsNotFixed'; +import HeightIcon from '@mui/icons-material/Height'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import RouteIcon from '@mui/icons-material/Route'; +import SouthIcon from '@mui/icons-material/South'; +import ShareLocationIcon from '@mui/icons-material/ShareLocation'; // TODO mover textos para arquivo de tradução export default function LaunchesPage() { @@ -68,26 +67,55 @@ export default function LaunchesPage() { } subheader={formatLaunchDatetime(launch.launch_datetime)} /> - + - - - {launch.launch_city} → {launch.landing_city} - - + + + + + + + + + {/* */} + {/* */} + + {launch.launch_city} + + + + + + + + + {launch.landing_city} + + - - - Altitude máxima - - - {launch.max_altitude.toLocaleString('pt-BR')} m - + } + label={convertAltitudeToKm(launch.max_altitude)} + color="primary" + variant="filled" + sx={{ alignSelf: 'flex-start', mt: 3 }} + /> - + ))} diff --git a/src/shared/utils/formatters.utils.ts b/src/shared/utils/formatters.utils.ts index 9069ee21..be08baad 100644 --- a/src/shared/utils/formatters.utils.ts +++ b/src/shared/utils/formatters.utils.ts @@ -17,5 +17,10 @@ export const formatLaunchDatetime = (datetime: string): string => { locale: ptBR }); - return `${formattedDate} ${date.toLocaleTimeString('pt-BR')}`; + return `${formattedDate} - ${date.toLocaleTimeString('pt-BR')} UTC`; +}; + +export const convertAltitudeToKm = (altitude: number): string => { + const altitudeInKm = altitude / 1000; + return `${altitudeInKm.toFixed(2)} km`; }; From 18a04bef318b9cc353383a6b6247cc3e699df3cd Mon Sep 17 00:00:00 2001 From: itMatos Date: Sat, 23 May 2026 11:06:57 -0300 Subject: [PATCH 09/13] feat: add launch details page with telemetry readings and improved navigation --- pages/launches/[launchName].tsx | 148 +++++++++++++++++++++++++++ pages/launches/index.tsx | 17 ++- src/shared/utils/formatters.utils.ts | 46 +++++++++ 3 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 pages/launches/[launchName].tsx diff --git a/pages/launches/[launchName].tsx b/pages/launches/[launchName].tsx new file mode 100644 index 00000000..2bf81561 --- /dev/null +++ b/pages/launches/[launchName].tsx @@ -0,0 +1,148 @@ +import { useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Grid, Stack, Typography } from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; +import { getLaunchContent } from '@/src/core/services/launches.service'; +import { formatLaunchDatetime, formatLaunchName, slugifyLaunchName } from '@/src/shared/utils/formatters.utils'; +import { LaunchRecord } from '@/src/shared/types/api/launches-api.types'; + +const renderTelemetryValue = (value: string | number): string => { + return typeof value === 'number' ? value.toLocaleString('pt-BR') : String(value); +}; + +export default function LaunchDetailsPage() { + const router = useRouter(); + const launchName = typeof router.query.launchName === 'string' ? router.query.launchName : ''; + const { launches, isLoadingAllLaunches, error } = useAllLaunches(); + const [records, setRecords] = useState([]); + const [isLoadingRecords, setIsLoadingRecords] = useState(false); + const [recordsError, setRecordsError] = useState(null); + + const launch = useMemo(() => { + return launches.find((launchItem) => slugifyLaunchName(launchItem.name) === launchName); + }, [launches, launchName]); + + useEffect(() => { + if (!launch) return; + + const controller = new AbortController(); + + const fetchLaunchRecords = async () => { + setIsLoadingRecords(true); + setRecordsError(null); + try { + const data = await getLaunchContent(launch.download_url); + if (!controller.signal.aborted) setRecords(data); + } catch (fetchError) { + if (fetchError instanceof Error && fetchError.name === 'AbortError') return; + setRecordsError( + fetchError instanceof Error ? fetchError.message : 'Não foi possível carregar os detalhes do lançamento.' + ); + } finally { + if (!controller.signal.aborted) setIsLoadingRecords(false); + } + }; + + fetchLaunchRecords(); + return () => controller.abort(); + }, [launch]); + + if (isLoadingAllLaunches) { + return ( + + + + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (!launch) { + return ( + + + + Lançamento não encontrado. + + + ); + } + + return ( + + + + + + + + + + {formatLaunchName(launch.name)} + + + {formatLaunchDatetime(launch.launch_datetime)} + + + + + + + + + + + + + + + Leituras do lançamento + + + {isLoadingRecords ? ( + + + + ) : recordsError ? ( + {recordsError} + ) : records.length === 0 ? ( + Nenhuma leitura encontrada para esse lançamento. + ) : ( + + {records.slice(0, 6).map((record, index) => ( + + + + + + {new Date(record.datetime).toLocaleString('pt-BR')} + + Altitude: {renderTelemetryValue(record.alt)} + Velocidade vertical: {renderTelemetryValue(record.vel_v)} + Velocidade horizontal: {renderTelemetryValue(record.vel_h)} + + + + + ))} + + )} + + + + ); +} diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index cbbcc6ce..fcb52445 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -1,17 +1,20 @@ import { useEffect } from 'react'; +import Link from 'next/link'; import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; -import { convertAltitudeToKm, formatLaunchDatetime, formatLaunchName } from '@/src/shared/utils/formatters.utils'; +import { + convertAltitudeToKm, + formatLaunchDatetime, + formatLaunchName, + slugifyLaunchName +} from '@/src/shared/utils/formatters.utils'; import { Alert, Box, Button, Card, CardActions, CardContent, CardHeader } from '@mui/material'; import { Chip, CircularProgress, Container, Stack, Typography } from '@mui/material'; import { Timeline, TimelineItem, TimelineSeparator, TimelineConnector } from '@mui/lab'; import { TimelineContent, TimelineDot, timelineItemClasses } from '@mui/lab'; import type {} from '@mui/lab/themeAugmentation'; import PinDropIcon from '@mui/icons-material/PinDrop'; -import GpsNotFixedIcon from '@mui/icons-material/GpsNotFixed'; import HeightIcon from '@mui/icons-material/Height'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import RouteIcon from '@mui/icons-material/Route'; -import SouthIcon from '@mui/icons-material/South'; import ShareLocationIcon from '@mui/icons-material/ShareLocation'; // TODO mover textos para arquivo de tradução @@ -113,7 +116,11 @@ export default function LaunchesPage() { - diff --git a/src/shared/utils/formatters.utils.ts b/src/shared/utils/formatters.utils.ts index be08baad..1aea5e10 100644 --- a/src/shared/utils/formatters.utils.ts +++ b/src/shared/utils/formatters.utils.ts @@ -1,6 +1,13 @@ import { format } from 'date-fns'; import { ptBR } from 'date-fns/locale'; +/** + * Extracts the display name for a launch, removing the prefix before " - " + * and the trailing ".json" suffix when present. + * + * @param name - Raw launch name returned by the API. + * @returns A cleaned, human-readable launch name. + */ export const formatLaunchName = (name: string): string => { const parts = name.split(' - '); const formattedName = parts.length > 1 ? parts[1].trim() : name; @@ -8,6 +15,39 @@ export const formatLaunchName = (name: string): string => { return formattedName.replace(/\.json$/i, ''); }; +/** + * Converts a launch name into a URL-safe slug. + * + * The result is normalized to lowercase, stripped of accents, and converted + * to kebab-case so it can be used safely in dynamic routes. + * + * Example: + * ```ts + * slugifyLaunchName('2026 - Lançamento de Teste.json'); + * // 'lancamento-de-teste' + * ``` + * + * @param name - Raw launch name returned by the API. + * @returns A slugified version of the launch name. + */ +export const slugifyLaunchName = (name: string): string => { + return formatLaunchName(name) + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +}; + +/** + * Formats a launch datetime using Portuguese locale rules. + * + * Dates from the current year omit the year in the formatted string; older + * dates include it. The current UTC time is appended in the existing format. + * + * @param datetime - ISO datetime string returned by the API. + * @returns A localized, human-readable launch datetime string. + */ export const formatLaunchDatetime = (datetime: string): string => { const date = new Date(datetime); const currentYear = new Date().getFullYear(); @@ -20,6 +60,12 @@ export const formatLaunchDatetime = (datetime: string): string => { return `${formattedDate} - ${date.toLocaleTimeString('pt-BR')} UTC`; }; +/** + * Converts altitude from meters to kilometers for display. + * + * @param altitude - Altitude value in meters. + * @returns The altitude formatted in kilometers with two decimal places. + */ export const convertAltitudeToKm = (altitude: number): string => { const altitudeInKm = altitude / 1000; return `${altitudeInKm.toFixed(2)} km`; From b1793c5c673b11822aec73eef79c535fb596a411 Mon Sep 17 00:00:00 2001 From: itMatos Date: Sat, 23 May 2026 11:22:27 -0300 Subject: [PATCH 10/13] feat: refactor launches page to use session storage for selected launch and improve data fetching logic --- pages/launches/[launchName].tsx | 121 ++++++++---------- pages/launches/index.tsx | 16 ++- .../launches/useGetLaunchContent.service.ts | 45 +++++++ 3 files changed, 111 insertions(+), 71 deletions(-) diff --git a/pages/launches/[launchName].tsx b/pages/launches/[launchName].tsx index 2bf81561..ea50ae11 100644 --- a/pages/launches/[launchName].tsx +++ b/pages/launches/[launchName].tsx @@ -3,10 +3,11 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Grid, Stack, Typography } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; -import { getLaunchContent } from '@/src/core/services/launches.service'; import { formatLaunchDatetime, formatLaunchName, slugifyLaunchName } from '@/src/shared/utils/formatters.utils'; -import { LaunchRecord } from '@/src/shared/types/api/launches-api.types'; +import { useGetLaunchContent } from '@/src/core/services/launches/useGetLaunchContent.service'; +import { LaunchSummary } from '@/src/shared/types/api/launches-api.types'; + +const SELECTED_LAUNCH_STORAGE_KEY = 'zenith-selected-launch'; const renderTelemetryValue = (value: string | number): string => { return typeof value === 'number' ? value.toLocaleString('pt-BR') : String(value); @@ -15,41 +16,60 @@ const renderTelemetryValue = (value: string | number): string => { export default function LaunchDetailsPage() { const router = useRouter(); const launchName = typeof router.query.launchName === 'string' ? router.query.launchName : ''; - const { launches, isLoadingAllLaunches, error } = useAllLaunches(); - const [records, setRecords] = useState([]); - const [isLoadingRecords, setIsLoadingRecords] = useState(false); - const [recordsError, setRecordsError] = useState(null); - - const launch = useMemo(() => { - return launches.find((launchItem) => slugifyLaunchName(launchItem.name) === launchName); - }, [launches, launchName]); + const [launch, setLaunch] = useState(null); useEffect(() => { - if (!launch) return; - - const controller = new AbortController(); - - const fetchLaunchRecords = async () => { - setIsLoadingRecords(true); - setRecordsError(null); - try { - const data = await getLaunchContent(launch.download_url); - if (!controller.signal.aborted) setRecords(data); - } catch (fetchError) { - if (fetchError instanceof Error && fetchError.name === 'AbortError') return; - setRecordsError( - fetchError instanceof Error ? fetchError.message : 'Não foi possível carregar os detalhes do lançamento.' - ); - } finally { - if (!controller.signal.aborted) setIsLoadingRecords(false); + const storedLaunch = sessionStorage.getItem(SELECTED_LAUNCH_STORAGE_KEY); + + if (!storedLaunch) { + setLaunch(null); + return; + } + + try { + const parsedLaunch: LaunchSummary = JSON.parse(storedLaunch); + const storedLaunchName = slugifyLaunchName(parsedLaunch.name); + + if (storedLaunchName !== launchName) { + setLaunch(null); + return; } - }; - fetchLaunchRecords(); - return () => controller.abort(); - }, [launch]); + setLaunch(parsedLaunch); + } catch { + setLaunch(null); + } + }, [launchName]); + + const { records, isLoadingRecords, recordsError } = useGetLaunchContent(launch?.download_url ?? ''); + + if (!launchName) { + return ( + + + + Lançamento não encontrado. + + + ); + } + + if (!launch) { + return ( + + + + Lançamento não encontrado nesta sessão. Volte para a lista e abra o detalhe novamente. + + + ); + } - if (isLoadingAllLaunches) { + if (isLoadingRecords) { return ( @@ -59,15 +79,15 @@ export default function LaunchDetailsPage() { ); } - if (error) { + if (recordsError) { return ( - {error} + {recordsError} ); } - if (!launch) { + if (records.length === 0) { return ( @@ -112,35 +132,6 @@ export default function LaunchDetailsPage() { Leituras do lançamento - - {isLoadingRecords ? ( - - - - ) : recordsError ? ( - {recordsError} - ) : records.length === 0 ? ( - Nenhuma leitura encontrada para esse lançamento. - ) : ( - - {records.slice(0, 6).map((record, index) => ( - - - - - - {new Date(record.datetime).toLocaleString('pt-BR')} - - Altitude: {renderTelemetryValue(record.alt)} - Velocidade vertical: {renderTelemetryValue(record.vel_v)} - Velocidade horizontal: {renderTelemetryValue(record.vel_h)} - - - - - ))} - - )} diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx index fcb52445..622f6033 100644 --- a/pages/launches/index.tsx +++ b/pages/launches/index.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import Link from 'next/link'; +import { useRouter } from 'next/router'; import { useAllLaunches } from '@/src/core/services/launches/useGetAllLaunches.service'; import { convertAltitudeToKm, @@ -17,12 +17,20 @@ import HeightIcon from '@mui/icons-material/Height'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ShareLocationIcon from '@mui/icons-material/ShareLocation'; +const SELECTED_LAUNCH_STORAGE_KEY = 'zenith-selected-launch'; + // TODO mover textos para arquivo de tradução export default function LaunchesPage() { const { launches, isLoadingAllLaunches, error } = useAllLaunches(); + const router = useRouter(); useEffect(() => {}, [isLoadingAllLaunches, launches]); + const handleLaunchDetails = (launch: (typeof launches)[number]) => { + sessionStorage.setItem(SELECTED_LAUNCH_STORAGE_KEY, JSON.stringify(launch)); + router.push(`/launches/${slugifyLaunchName(launch.name)}`); + }; + return ( @@ -116,11 +124,7 @@ export default function LaunchesPage() { - diff --git a/src/core/services/launches/useGetLaunchContent.service.ts b/src/core/services/launches/useGetLaunchContent.service.ts index e69de29b..dbe2aa19 100644 --- a/src/core/services/launches/useGetLaunchContent.service.ts +++ b/src/core/services/launches/useGetLaunchContent.service.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; +import { getLaunchContent } from '../launches.service'; +import { LaunchRecord } from '@/src/shared/types/api/launches-api.types'; + +export const useGetLaunchContent = (downloadUrl: string) => { + const [records, setRecords] = useState([]); + const [isLoadingRecords, setIsLoadingRecords] = useState(false); + const [recordsError, setRecordsError] = useState(null); + + useEffect(() => { + if (!downloadUrl) return; + + const controller = new AbortController(); + + const fetchLaunchContent = async () => { + setIsLoadingRecords(true); + setRecordsError(null); + + try { + const data = await getLaunchContent(downloadUrl); + if (!controller.signal.aborted) { + setRecords(data); + } + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') { + return; + } + + setRecordsError(err instanceof Error ? err.message : 'Ocorreu um erro inesperado'); + } finally { + if (!controller.signal.aborted) { + setIsLoadingRecords(false); + } + } + }; + + fetchLaunchContent(); + + return () => { + controller.abort(); + }; + }, [downloadUrl]); + + return { records, isLoadingRecords, recordsError }; +}; From 430da6ae60f0cc5e84786d26f911c36db6011190 Mon Sep 17 00:00:00 2001 From: itMatos Date: Sat, 23 May 2026 11:38:33 -0300 Subject: [PATCH 11/13] feat: enhance launch details page with dynamic map component and improved loading state handling --- pages/launches/[launchName].tsx | 26 +++++++++++++------ pages/launches/index.tsx | 2 +- .../launches/useGetLaunchContent.service.ts | 6 ++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pages/launches/[launchName].tsx b/pages/launches/[launchName].tsx index ea50ae11..96fbae09 100644 --- a/pages/launches/[launchName].tsx +++ b/pages/launches/[launchName].tsx @@ -1,23 +1,31 @@ import { useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Grid, Stack, Typography } from '@mui/material'; +import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Stack, Typography } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { formatLaunchDatetime, formatLaunchName, slugifyLaunchName } from '@/src/shared/utils/formatters.utils'; import { useGetLaunchContent } from '@/src/core/services/launches/useGetLaunchContent.service'; import { LaunchSummary } from '@/src/shared/types/api/launches-api.types'; +import dynamic from 'next/dynamic'; const SELECTED_LAUNCH_STORAGE_KEY = 'zenith-selected-launch'; -const renderTelemetryValue = (value: string | number): string => { - return typeof value === 'number' ? value.toLocaleString('pt-BR') : String(value); -}; - export default function LaunchDetailsPage() { const router = useRouter(); const launchName = typeof router.query.launchName === 'string' ? router.query.launchName : ''; const [launch, setLaunch] = useState(null); + const { records, isLoadingRecords, recordsError } = useGetLaunchContent(launch?.download_url ?? '', Boolean(launch)); + + const Map = useMemo( + () => + dynamic(() => import('@/src/components/Map/Map'), { + loading: () =>

A map is loading

, + ssr: false + }), + [] + ); + useEffect(() => { const storedLaunch = sessionStorage.getItem(SELECTED_LAUNCH_STORAGE_KEY); @@ -41,8 +49,6 @@ export default function LaunchDetailsPage() { } }, [launchName]); - const { records, isLoadingRecords, recordsError } = useGetLaunchContent(launch?.download_url ?? ''); - if (!launchName) { return ( @@ -101,7 +107,7 @@ export default function LaunchDetailsPage() { } return ( - +