Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"printWidth": 130,
"bracketSameLine": true,
"bracketSameLine": false,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
Expand Down
2 changes: 1 addition & 1 deletion components/home-components/HeroSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function HomepageHeroSection() {
const title = t('homePage:homePageHero.title');
const subtitle = t('homePage:homePageHero.subtitle');

useEffect(() => {
useEffect(() => {
let isMounted = true;

const initializeParticles = async () => {
Expand Down
235 changes: 137 additions & 98 deletions pages/launches/[launchName].tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,62 @@
import { useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import React, { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router';
import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Stack, Typography } from '@mui/material';
import Link from 'next/link';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { formatLaunchDatetime, formatLaunchName, slugifyLaunchName } from '@/src/shared/utils/formatters.utils';
import { Alert, Box, Button, Card, CardContent, Chip, CircularProgress, Container, Stack, Typography } from '@mui/material';
import {
formatAltitude,
formatAltitudeInKm,
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';
import LaunchAndLandingCities from '@/src/components/LaunchAndLandingCities/LaunchAndLandingCities';

const SELECTED_LAUNCH_STORAGE_KEY = 'zenith-selected-launch';

// TODO mover textos para arquivo de tradução
// TODO mover para arquivos de formatters
const formatMissionDuration = (start: string, end: string) => {
const durationInSeconds = Math.max(0, Math.round((new Date(end).getTime() - new Date(start).getTime()) / 1000));
const hours = Math.floor(durationInSeconds / 3600);
const minutes = Math.floor((durationInSeconds % 3600) / 60);
const seconds = durationInSeconds % 60;

return `${hours}h ${minutes}min ${seconds}seg`;
};

export default function LaunchDetailsPage() {
const router = useRouter();
const launchName = typeof router.query.launchName === 'string' ? router.query.launchName : '';
const [launch, setLaunch] = useState<LaunchSummary | null>(null);
const [launchResolved, setLaunchResolved] = useState(false);

const { records, isLoadingRecords, recordsError } = useGetLaunchContent(launch?.download_url ?? '', Boolean(launch));

const Map = useMemo(
const MapTrajectory = useMemo(
() =>
dynamic(() => import('@/src/components/Map/Map'), {
dynamic(() => import('@/src/components/Map/MapTrajectory'), {
loading: () => <p>A map is loading</p>,
ssr: false
}),
[]
);

useEffect(() => {
if (!router.isReady) {
return;
}

setLaunchResolved(false);

const storedLaunch = sessionStorage.getItem(SELECTED_LAUNCH_STORAGE_KEY);

if (!storedLaunch) {
setLaunch(null);
setLaunchResolved(true);
return;
}

Expand All @@ -40,110 +66,123 @@ export default function LaunchDetailsPage() {

if (storedLaunchName !== launchName) {
setLaunch(null);
setLaunchResolved(true);
return;
}

setLaunch(parsedLaunch);
} catch {
setLaunch(null);
} finally {
setLaunchResolved(true);
}
}, [launchName]);
}, [launchName, router.isReady]);

if (!launchName) {
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Stack spacing={3}>
<Button component={Link} href="/launches" startIcon={<ArrowBackIcon />} sx={{ width: 'fit-content' }}>
Voltar para lançamentos
</Button>
<Alert severity="info">Lançamento não encontrado.</Alert>
</Stack>
</Container>
);
}

if (!launch) {
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Stack spacing={3}>
<Button component={Link} href="/launches" startIcon={<ArrowBackIcon />} sx={{ width: 'fit-content' }}>
return (
<Box sx={{ width: '100%' }}>
<Container maxWidth="xl" sx={{ py: 2 }}>
<Stack spacing={1}>
<Button
component={Link}
href="/launches"
startIcon={<ArrowBackIcon />}
sx={{ width: 'fit-content', margin: 0, padding: 0 }}
>
Voltar para lançamentos
</Button>
<Alert severity="info">Lançamento não encontrado nesta sessão. Volte para a lista e abra o detalhe novamente.</Alert>
</Stack>
</Container>
);
}

if (isLoadingRecords) {
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', py: 10 }}>
<CircularProgress />
</Box>
</Container>
);
}

if (recordsError) {
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Alert severity="error">{recordsError}</Alert>
</Container>
);
}

if (records.length === 0) {
return (
<Container maxWidth="md" sx={{ py: 6 }}>
<Stack spacing={3}>
<Button component={Link} href="/launches" startIcon={<ArrowBackIcon />} sx={{ width: 'fit-content' }}>
Voltar para lançamentos
</Button>
<Alert severity="info">Lançamento não encontrado.</Alert>
{!router.isReady ||
(!launchResolved && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 10 }}>
<CircularProgress />
</Box>
))}

{!launchName ||
(!launch && (
<Alert severity="info">
{!launchName
? 'Lançamento não encontrado.'
: 'Lançamento não encontrado nesta sessão. Volte para a lista e abra o detalhe novamente.'}
</Alert>
))}

{isLoadingRecords && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 10 }}>
<CircularProgress />
</Box>
)}

{recordsError && <Alert severity="error">{recordsError}</Alert>}

{records.length === 0 && <Alert severity="info">Lançamento não encontrado.</Alert>}

{launch && records.length > 0 && (
<React.Fragment>
<Card elevation={4} sx={{ borderRadius: 3 }}>
<CardContent>
<Stack spacing={3}>
<Box>
<Typography variant="h4" component="h1" sx={{ fontWeight: 800 }}>
{formatLaunchName(launch.name)}
</Typography>
<Typography variant="body1" color="text.secondary">
{formatLaunchDatetime(launch.launch_datetime)}
</Typography>
</Box>

<LaunchAndLandingCities startLabel={launch.launch_city} endLabel={launch.landing_city} />

<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap' }} useFlexGap>
{/* <Chip label="Altitude máxima" color="primary" variant="outlined" /> */}
<Chip
label={`Altitude máxima: ${formatAltitude(launch.max_altitude)} (${formatAltitudeInKm(
launch.max_altitude
)})`}
variant="outlined"
color="primary"
/>
<Chip
label={`Duração aprox.: ${formatMissionDuration(
records[0].datetime,
records[records.length - 1].datetime
)}`}
variant="outlined"
/>
</Stack>
</Stack>
</CardContent>
</Card>

<Box
sx={{
width: '100%',
maxWidth: '100%',
mx: 'auto',
overflow: 'hidden'
}}
>
<Box>
<Typography variant="h5" component="h2" sx={{ fontWeight: 700, mb: 2 }}>
Trajetória do lançamento
</Typography>
</Box>

<MapTrajectory
position={[records[0]?.lat, records[0]?.lon]}
zoom={20}
trajectory={records.map((r) => [r.lat, r.lon])}
trajectoryRecords={records}
landingCity={launch.landing_city}
lineColor="#f44336"
lineWeight={4}
mapHeight="100vh"
/>
</Box>
</React.Fragment>
)}
</Stack>
</Container>
);
}

return (
<Container maxWidth="lg" sx={{ py: 6 }}>
<Stack spacing={3}>
<Button component={Link} href="/launches" startIcon={<ArrowBackIcon />} sx={{ width: 'fit-content' }}>
Voltar para lançamentos
</Button>

<Card elevation={4} sx={{ borderRadius: 3 }}>
<CardContent>
<Stack spacing={2}>
<Box>
<Typography variant="h4" component="h1" sx={{ fontWeight: 800 }}>
{formatLaunchName(launch.name)}
</Typography>
<Typography variant="body1" color="text.secondary">
{formatLaunchDatetime(launch.launch_datetime)}
</Typography>
</Box>

<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap' }} useFlexGap>
<Chip label={launch.launch_city} color="primary" variant="outlined" />
<Chip label={launch.landing_city} color="primary" variant="outlined" />
<Chip label={`${launch.max_altitude.toLocaleString('pt-BR')} m`} variant="outlined" />
</Stack>
</Stack>
</CardContent>
</Card>

<Box>
<Typography variant="h5" component="h2" sx={{ fontWeight: 700, mb: 2 }}>
Leituras do lançamento
</Typography>
</Box>

<Box>
<Map position={[records[0]?.lat, records[0]?.lon]} zoom={20} />
</Box>
</Stack>
</Container>
</Box>
);
}
Loading
Loading