diff --git a/pages/_app.jsx b/pages/_app.jsx
index 5929631..c64b171 100644
--- a/pages/_app.jsx
+++ b/pages/_app.jsx
@@ -1,14 +1,16 @@
import React from 'react';
import Head from 'next/head';
import 'styles/global.css';
+import RouteProgressBar from '@/src/components/RouteProgressBar/RouteProgressBar';
export default function MyApp({ Component, pageProps }) {
-
+
return (
<>
+
>
)
diff --git a/pages/launches/[launchName].tsx b/pages/launches/[launchName].tsx
index c6384a2..266dc1e 100644
--- a/pages/launches/[launchName].tsx
+++ b/pages/launches/[launchName].tsx
@@ -1,7 +1,10 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Stack, Typography } from '@mui/material';
+import { Alert, Box, Button, Card, CardContent, CircularProgress, Container, Stack, Typography } from '@mui/material';
+import HeightIcon from '@mui/icons-material/Height';
+import ScheduleIcon from '@mui/icons-material/Schedule';
+import RouteIcon from '@mui/icons-material/Route';
import {
formatAltitude,
formatAltitudeInKm,
@@ -10,9 +13,11 @@ import {
slugifyLaunchName
} from '@/src/shared/utils/formatters.utils';
import { useGetLaunchContent } from '@/src/core/services/launches/useGetLaunchContent.service';
+import { getAllLaunches } from '@/src/core/services/launches.service';
import { LaunchSummary } from '@/src/shared/types/api/launches-api.types';
import dynamic from 'next/dynamic';
import LaunchAndLandingCities from '@/src/components/LaunchAndLandingCities/LaunchAndLandingCities';
+import StatCard from '@/src/components/StatCard/StatCard';
const SELECTED_LAUNCH_STORAGE_KEY = 'zenith-selected-launch';
@@ -32,6 +37,7 @@ export default function LaunchDetailsPage() {
const launchName = typeof router.query.launchName === 'string' ? router.query.launchName : '';
const [launch, setLaunch] = useState(null);
const [launchResolved, setLaunchResolved] = useState(false);
+ const [launchNotFound, setLaunchNotFound] = useState(false);
const { records, isLoadingRecords, recordsError } = useGetLaunchContent(launch?.download_url ?? '', Boolean(launch));
@@ -44,45 +50,82 @@ export default function LaunchDetailsPage() {
[]
);
+ useEffect(() => {
+ router.prefetch('/launches');
+ }, [router]);
+
useEffect(() => {
if (!router.isReady) {
return;
}
- setLaunchResolved(false);
+ const controller = new AbortController();
- const storedLaunch = sessionStorage.getItem(SELECTED_LAUNCH_STORAGE_KEY);
+ const resolveFromStorage = (): LaunchSummary | null => {
+ const storedLaunch = sessionStorage.getItem(SELECTED_LAUNCH_STORAGE_KEY);
+ if (!storedLaunch) {
+ return null;
+ }
- if (!storedLaunch) {
- setLaunch(null);
- setLaunchResolved(true);
- return;
- }
+ try {
+ const parsedLaunch: LaunchSummary = JSON.parse(storedLaunch);
+ return slugifyLaunchName(parsedLaunch.name) === launchName ? parsedLaunch : null;
+ } catch {
+ return null;
+ }
+ };
- try {
- const parsedLaunch: LaunchSummary = JSON.parse(storedLaunch);
- const storedLaunchName = slugifyLaunchName(parsedLaunch.name);
+ const resolveLaunch = async () => {
+ setLaunchResolved(false);
+ setLaunchNotFound(false);
- if (storedLaunchName !== launchName) {
- setLaunch(null);
+ const storedMatch = resolveFromStorage();
+ if (storedMatch) {
+ setLaunch(storedMatch);
setLaunchResolved(true);
return;
}
- setLaunch(parsedLaunch);
- } catch {
- setLaunch(null);
- } finally {
- setLaunchResolved(true);
- }
+ try {
+ const allLaunches = await getAllLaunches();
+ if (controller.signal.aborted) {
+ return;
+ }
+
+ const matchedLaunch = allLaunches.find((item) => slugifyLaunchName(item.name) === launchName) ?? null;
+
+ if (matchedLaunch) {
+ sessionStorage.setItem(SELECTED_LAUNCH_STORAGE_KEY, JSON.stringify(matchedLaunch));
+ setLaunch(matchedLaunch);
+ } else {
+ setLaunch(null);
+ setLaunchNotFound(true);
+ }
+ } catch {
+ if (!controller.signal.aborted) {
+ setLaunch(null);
+ setLaunchNotFound(true);
+ }
+ } finally {
+ if (!controller.signal.aborted) {
+ setLaunchResolved(true);
+ }
+ }
+ };
+
+ resolveLaunch();
+
+ return () => {
+ controller.abort();
+ };
}, [launchName, router.isReady]);
return (
-
+
))}
- {!launchName ||
- (!launch && (
-
- {!launchName
- ? 'Lançamento não encontrado.'
- : 'Lançamento não encontrado nesta sessão. Volte para a lista e abra o detalhe novamente.'}
-
- ))}
+ {launchResolved && (!launchName || (!launch && launchNotFound)) && (
+ Lançamento não encontrado.
+ )}
{isLoadingRecords && (
@@ -129,58 +167,50 @@ export default function LaunchDetailsPage() {
-
-
- {/* */}
- }
+ icon={}
/>
- }
+ />
+ }
/>
-
-
-
- Trajetória do lançamento
-
-
-
- [r.lat, r.lon])}
- trajectoryRecords={records}
- landingCity={launch.landing_city}
- lineColor="#f44336"
- lineWeight={4}
- mapHeight="100vh"
- />
-
+
+ Trajetória do lançamento
+
)}
+
+ {launch && records.length > 0 && (
+
+ [r.lat, r.lon])}
+ trajectoryRecords={records}
+ landingCity={launch.landing_city}
+ lineColor="#f44336"
+ lineWeight={4}
+ mapHeight="600px"
+ />
+
+ )}
);
}
diff --git a/pages/launches/index.tsx b/pages/launches/index.tsx
index 21619b3..a64d9db 100644
--- a/pages/launches/index.tsx
+++ b/pages/launches/index.tsx
@@ -10,6 +10,7 @@ import HeadTags from '@/components/general/HeadTags';
import NavBar from '@/src/components/Navbar/NavBar';
import HeroSection from '@/components/projects-components/HeroSection';
import { LOCALE } from '@/src/shared/consts/locales.const';
+import { BACKGROUND_COLOR } from '@/src/shared/styles/colors';
const SELECTED_LAUNCH_STORAGE_KEY = 'zenith-selected-launch';
const SCROLL_POSITION_KEY = 'zenith-launches-scroll';
@@ -18,10 +19,7 @@ const SCROLL_POSITION_KEY = 'zenith-launches-scroll';
export default function LaunchesPage() {
const { launches, isLoadingAllLaunches, error } = useAllLaunches();
const router = useRouter();
-
const { t } = useTranslation();
- const launchesTitle = t('allLaunches:allLaunchesPage.header.title');
- const launchesDescription = t('allLaunches:allLaunchesPage.header.description');
useEffect(() => {
if (isLoadingAllLaunches || launches.length === 0) return;
@@ -33,21 +31,27 @@ export default function LaunchesPage() {
}
}, [isLoadingAllLaunches, launches]);
+ useEffect(() => {
+ if (isLoadingAllLaunches || launches.length === 0) return;
+
+ router.prefetch('/launches/[launchName]', `/launches/${slugifyLaunchName(launches[0].name)}`);
+ }, [isLoadingAllLaunches, launches, router]);
+
const handleLaunchDetails = (launch: (typeof launches)[number]) => {
sessionStorage.setItem(SELECTED_LAUNCH_STORAGE_KEY, JSON.stringify(launch));
sessionStorage.setItem(SCROLL_POSITION_KEY, String(window.scrollY));
router.push(`/launches/${slugifyLaunchName(launch.name)}`);
};
+ const pageName = t(LOCALE.LAUNCHES.META_TAGS.PAGE_NAME);
+ const headTagsTitle = t(LOCALE.LAUNCHES.META_TAGS.TITLE);
+ const pageTitle = t(LOCALE.LAUNCHES.META_TAGS.PAGE_TITLE);
+ const pageDescription = t(LOCALE.LAUNCHES.META_TAGS.DESCRIPTION);
+ const lang = t(LOCALE.LAUNCHES.META_TAGS.LANG);
+
return (
<>
-
+
-
- {/*
-
- {launchesTitle}
-
-
- {launchesDescription}
-
- */}
+
+
+ {error && (
+
+ {error}
+
+ )}
- {error && (
-
- {error}
-
- )}
+ {isLoadingAllLaunches && (
+
+
+
+ )}
- {isLoadingAllLaunches && (
-
-
-
- )}
+ {!isLoadingAllLaunches && launches.length === 0 && Nenhum lançamento encontrado.}
- {!isLoadingAllLaunches && launches.length === 0 && Nenhum lançamento encontrado.}
-
- {!isLoadingAllLaunches && launches.length > 0 && (
-
- {launches.map((launch) => (
-
- ))}
-
- )}
-
+ {!isLoadingAllLaunches && launches.length > 0 && (
+
+ {launches.map((launch) => (
+
+ ))}
+
+ )}
+
+
>
);
}
+
+const pageBackgroundSx = { backgroundColor: BACKGROUND_COLOR };
+const containerSx = { py: 6 };
+const errorAlertSx = { mb: 3 };
+const loadingBoxSx = { display: 'flex', justifyContent: 'center', py: 10 };
+const launchesGridSx = {
+ display: 'grid',
+ gap: 3,
+ gridTemplateColumns: {
+ xs: '1fr',
+ sm: 'repeat(2, minmax(0, 1fr))',
+ md: 'repeat(2, minmax(0, 1fr))'
+ }
+};
diff --git a/src/components/LaunchAndLandingCities/LaunchAndLandingCities.tsx b/src/components/LaunchAndLandingCities/LaunchAndLandingCities.tsx
index 1eb7cce..d02e1ec 100644
--- a/src/components/LaunchAndLandingCities/LaunchAndLandingCities.tsx
+++ b/src/components/LaunchAndLandingCities/LaunchAndLandingCities.tsx
@@ -12,40 +12,51 @@ import type {} from '@mui/lab/themeAugmentation';
import ShareLocationIcon from '@mui/icons-material/ShareLocation';
import PinDropIcon from '@mui/icons-material/PinDrop';
import { LaunchAndLandingCitiesProps } from '@/src/shared/props/components/launch-and-landing.props';
+import { colors } from '@/src/shared/styles/colors';
export default function LaunchAndLandingCities({
startLabel,
endLabel,
- startIcon = ,
- endIcon =
+ startIcon = ,
+ endIcon =
}: LaunchAndLandingCitiesProps) {
return (
-
+ }}
+ >
+
- {startIcon}
-
+ {startIcon}
- {startLabel}
+ {startLabel}
-
+
+
+
+
+
+
+
+
- {endIcon}
+ {endIcon}
- {endLabel}
+ {endLabel}
);
}
+
+const timelineContentSx = { display: 'flex', alignItems: 'center', py: 0, my: 0 };
+const timelineConnectorSx = { minHeight: 32 };
+const connectorItemSx = { minHeight: 0, mb: 0, py: 0 };
+const connectorSeparatorSx = { alignItems: 'center', width: 24, pl: 2 };
diff --git a/src/components/LaunchCard/LaunchSummaryCard.tsx b/src/components/LaunchCard/LaunchSummaryCard.tsx
index 2940192..b841826 100644
--- a/src/components/LaunchCard/LaunchSummaryCard.tsx
+++ b/src/components/LaunchCard/LaunchSummaryCard.tsx
@@ -4,39 +4,58 @@ import HeightIcon from '@mui/icons-material/Height';
import LaunchAndLandingCities from '@/src/components/LaunchAndLandingCities/LaunchAndLandingCities';
import { formatAltitudeInKm, formatLaunchDatetime, formatLaunchName } from '@/src/shared/utils/formatters.utils';
import { LaunchSummaryCardProps } from '@/src/shared/props/components/launch-summary-card.props';
+import MapIcon from '@mui/icons-material/Map';
+import { colors } from '@/src/shared/styles/colors';
export default function LaunchSummaryCard({ launch, onDetailsClick }: LaunchSummaryCardProps) {
return (
-
+
- {formatLaunchName(launch.name)}
-
- }
- subheader={formatLaunchDatetime(launch.launch_datetime)}
- />
-
-
-
-
-
+
+
+ {formatLaunchName(launch.name)}
+
}
label={formatAltitudeInKm(launch.max_altitude)}
color="primary"
variant="filled"
- sx={{ alignSelf: 'flex-start', mt: 3 }}
+ sx={chipSx}
/>
+ }
+ subheader={formatLaunchDatetime(launch.launch_datetime)}
+ />
+
+
+
+
+
-
- } onClick={() => onDetailsClick(launch)}>
+
+ }
+ endIcon={}
+ onClick={() => onDetailsClick(launch)}
+ sx={detailsButtonSx}
+ >
Ver trajetória
);
}
+
+const cardSx = { height: '100%', borderRadius: 3 };
+const chipSx = { backgroundColor: colors.primary[600] };
+const cardHeaderSx = { pb: 0 };
+const titleBoxSx = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2 };
+const titleTypographySx = { fontWeight: 700 };
+const cardContentSx = { py: 0, mb: 0 };
+const citiesBoxSx = { display: 'flex', alignItems: 'flex-start' };
+const cardActionsSx = { justifyContent: 'flex-end', pt: 0 };
+const detailsButtonSx = { color: colors.secondary[500] };
diff --git a/src/components/Map/MapTrajectory.tsx b/src/components/Map/MapTrajectory.tsx
index f5e8de3..dd29e28 100644
--- a/src/components/Map/MapTrajectory.tsx
+++ b/src/components/Map/MapTrajectory.tsx
@@ -1,11 +1,15 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import L from 'leaflet';
import { LayersControl, MapContainer, Marker, Polyline, Popup, TileLayer, useMap } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-defaulticon-compatibility';
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css';
+import FullscreenIcon from '@mui/icons-material/Fullscreen';
+import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
+import { IconButton, Tooltip } from '@mui/material';
import { MapProps } from '@/src/shared/types/map.types';
import { formatAltitudeInKm, formatLaunchDatetime } from '@/src/shared/utils/formatters.utils';
+import { colors } from '@/src/shared/styles/colors';
const parachutIconUrl = '/images/markersSondehub/parachute.svg';
const payloadNotRecoveredIconUrl = '/images/markersSondehub/payload-not-recovered.png';
@@ -100,8 +104,9 @@ export default function MapTrajectory(props: MapProps) {
landingCity = '',
lineColor = '#d32f2f',
lineWeight = 2,
- mapHeight = '100vh'
+ mapHeight = '360px'
} = props;
+ const [isFullscreen, setIsFullscreen] = useState(false);
const hasTrajectory = trajectory.length > 1;
const startPosition = hasTrajectory ? trajectory[0] : position;
const endPosition = hasTrajectory ? trajectory[trajectory.length - 1] : position;
@@ -113,18 +118,69 @@ export default function MapTrajectory(props: MapProps) {
);
const finalMarkerIcon = isUnknownEndPoint ? unknownEndMarkerIcon : endMarkerIcon;
+ useEffect(() => {
+ if (!isFullscreen) {
+ return;
+ }
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setIsFullscreen(false);
+ }
+ };
+
+ document.body.style.overflow = 'hidden';
+ window.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.body.style.overflow = '';
+ window.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [isFullscreen]);
+
return (
-
+
+
+ setIsFullscreen((current) => !current)}
+ sx={{
+ position: 'absolute',
+ top: 90,
+ left: 8,
+ zIndex: 1400,
+ width: 40,
+ height: 40,
+ backgroundColor: 'background.paper',
+ boxShadow: 1,
+ '&:hover': { backgroundColor: 'background.paper' }
+ }}
+ >
+ {isFullscreen ? : }
+
+
+
-
+
-
+
{
+ const handleStart = () => setIsNavigating(true);
+ const handleDone = () => setIsNavigating(false);
+
+ router.events.on('routeChangeStart', handleStart);
+ router.events.on('routeChangeComplete', handleDone);
+ router.events.on('routeChangeError', handleDone);
+
+ return () => {
+ router.events.off('routeChangeStart', handleStart);
+ router.events.off('routeChangeComplete', handleDone);
+ router.events.off('routeChangeError', handleDone);
+ };
+ }, [router]);
+
+ if (!isNavigating) return null;
+
+ return ;
+}
+
+const barSx = {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ height: 3,
+ width: '100%',
+ zIndex: 2000,
+ backgroundColor: colors.secondary[500],
+ transformOrigin: 'left',
+ animation: 'route-progress-bar 1.2s ease-in-out infinite',
+ '@keyframes route-progress-bar': {
+ '0%': { transform: 'scaleX(0)', opacity: 1 },
+ '70%': { transform: 'scaleX(0.85)', opacity: 1 },
+ '100%': { transform: 'scaleX(0.95)', opacity: 0.6 }
+ }
+};
diff --git a/src/components/StatCard/StatCard.tsx b/src/components/StatCard/StatCard.tsx
new file mode 100644
index 0000000..162431b
--- /dev/null
+++ b/src/components/StatCard/StatCard.tsx
@@ -0,0 +1,50 @@
+import { ReactNode } from 'react';
+import { Box, Paper, Typography } from '@mui/material';
+import { colors } from '@/src/shared/styles/colors';
+
+type StatCardProps = {
+ label: string;
+ value: ReactNode;
+ icon: ReactNode;
+};
+
+export default function StatCard({ label, value, icon }: StatCardProps) {
+ return (
+
+
+
+ {label}
+
+ {icon}
+
+ {typeof value === 'string' ? (
+
+ {value}
+
+ ) : (
+ value
+ )}
+
+ );
+}
+
+const cardSx = {
+ flex: '1 1 200px',
+ minWidth: 200,
+ borderRadius: 3,
+ border: '1px solid',
+ borderColor: 'divider',
+ p: 2.5
+};
+const headerSx = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 };
+const iconCircleSx = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 36,
+ height: 36,
+ borderRadius: '50%',
+ backgroundColor: colors.primary[50],
+ color: colors.primary[600]
+};
+const valueSx = { fontWeight: 700 };
diff --git a/src/shared/styles/colors.ts b/src/shared/styles/colors.ts
new file mode 100644
index 0000000..26803f9
--- /dev/null
+++ b/src/shared/styles/colors.ts
@@ -0,0 +1,48 @@
+export const BACKGROUND_COLOR = '#EFEFEF' as const;
+
+export const ZINC950 = '#09090B' as const;
+
+export const colors = {
+ // O Azul (#0193ED) - Ideal para a marca principal, links e botões primários
+ primary: {
+ 50: '#eff7ff',
+ 100: '#deefff',
+ 200: '#b6e1ff',
+ 300: '#75c9ff',
+ 400: '#2db0ff',
+ 500: '#0193ed', // <-- cor base
+ 600: '#007cd3',
+ 700: '#0062aa',
+ 800: '#00518c',
+ 900: '#064474',
+ 950: '#042b4d'
+ },
+ // O Laranja (#F28705) - Ideal para botões de ação (Call to Action), destaque secundário
+ secondary: {
+ 50: '#fff5d4',
+ 100: '#ffe8a7',
+ 200: '#ffd770',
+ 300: '#ffba36',
+ 400: '#ffa30f',
+ 500: '#f28705', // <-- cor base
+ 600: '#c86606',
+ 700: '#9e500e',
+ 800: '#7f430f',
+ 900: '#452005',
+ 950: '#452009'
+ },
+ // O Amarelo (#FED329) - Ideal para pequenos detalhes, ícones, alertas e "energia"
+ accent: {
+ 50: '#fffce6',
+ 100: '#fefce8',
+ 200: '#fffac2',
+ 300: '#fff188',
+ 400: '#ffe144',
+ 500: '#fed329', // <-- cor base
+ 600: '#eeb504',
+ 700: '#cd8b01',
+ 800: '#a46204',
+ 900: '#874d0c',
+ 950: '#733f10'
+ }
+};
diff --git a/src/shared/utils/formatters.utils.ts b/src/shared/utils/formatters.utils.ts
index c335333..9d8a255 100644
--- a/src/shared/utils/formatters.utils.ts
+++ b/src/shared/utils/formatters.utils.ts
@@ -61,6 +61,6 @@ export const formatLaunchDatetime = (datetime: string): string => {
};
export const formatAltitude = (altitude: number) =>
- new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(altitude) + ' m';
+ new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(altitude) + ' m';
export const formatAltitudeInKm = (altitude: number) =>
- `${new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(altitude / 1000)} km`;
+ `${new Intl.NumberFormat('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(altitude / 1000)} km`;