Updates to weather modal
All checks were successful
Deploy to production / build (push) Successful in 11s

This commit is contained in:
Sean Edmunds
2026-07-14 16:30:41 -05:00
parent 8bc8680152
commit 78d887de9a
7 changed files with 362 additions and 104 deletions

View File

@@ -3,52 +3,10 @@ import { useCommonHooks } from 'hooks';
import { FaCloud, FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { WiBarometer, WiHumidity, WiRain, WiSunrise, WiSunset } from 'react-icons/wi';
import { textClasses } from 'styles';
import type { MeteoWeather } from 'types';
import { type MeteoWeather, WeatherCodes } from 'types';
import WeatherFeaturePill from './WeatherFeaturePill';
import WeatherIcon from './WeatherIcon';
const WeatherCodes: { [key: number]: string } = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Fog',
48: 'Depositing rime fog',
51: 'Drizzle: Light',
53: 'Drizzle: Moderate',
55: 'Drizzle: Dense',
56: 'Freezing Drizzle: Light',
57: 'Freezing Drizzle: Dense',
61: 'Rain: Slight',
63: 'Rain: Moderate',
65: 'Rain: Heavy',
66: 'Freezing Rain: Light',
67: 'Freezing Rain: Heavy',
71: 'Snowfall: Slight',
73: 'Snowfall: Moderate',
75: 'Snowfall: Heavy',
77: 'Snow grains',
80: 'Rain showers: Slight',
81: 'Rain showers: Moderate',
82: 'Rain showers: Violent',
85: 'Snow showers: Slight',
86: 'Snow showers: Heavy',
95: 'Thunderstorm: Slight or moderate',
96: 'Thunderstorm with slight hail',
99: 'Thunderstorm with heavy hail',
} as const;
export default function CurrentWeather({
current,
}: {
@@ -321,7 +279,6 @@ export default function CurrentWeather({
<FaSun
style={{
color: 'var(--mantine-color-accent-0)',
transform: `rotate(${-45 + (current.current.wind_direction_10m || 0)}deg)`,
}}
/>
</Box>

View File

@@ -1,52 +1,16 @@
import { Card, Group, SimpleGrid, Stack, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { textClasses } from 'styles';
import type { Daily } from 'types';
import { type Daily, WeatherCodes } from 'types';
import WeatherIcon from './WeatherIcon';
const WeatherCodes: { [key: number]: string } = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Fog',
48: 'Depositing rime fog',
51: 'Drizzle: Light',
53: 'Drizzle: Moderate',
55: 'Drizzle: Dense',
56: 'Freezing Drizzle: Light',
57: 'Freezing Drizzle: Dense',
61: 'Rain: Slight',
63: 'Rain: Moderate',
65: 'Rain: Heavy',
66: 'Freezing Rain: Light',
67: 'Freezing Rain: Heavy',
71: 'Snowfall: Slight',
73: 'Snowfall: Moderate',
75: 'Snowfall: Heavy',
77: 'Snow grains',
80: 'Rain showers: Slight',
81: 'Rain showers: Moderate',
82: 'Rain showers: Violent',
85: 'Snow showers: Slight',
86: 'Snow showers: Heavy',
95: 'Thunderstorm: Slight or moderate',
96: 'Thunderstorm with slight hail',
99: 'Thunderstorm with heavy hail',
} as const;
export default function ExtendedForecastCard({ daily }: { daily: Daily | undefined }) {
export default function ExtendedForecastCard({
daily,
setSelectedDay,
}: {
daily: Daily | undefined;
setSelectedDay: any;
}) {
const { isMobile, theme } = useCommonHooks();
const forecast = daily?.time.map((_: any, index: number) => {
const time = new Date(daily.time[index]);
@@ -59,8 +23,8 @@ export default function ExtendedForecastCard({ daily }: { daily: Daily | undefin
precipitationHours: daily.precipitation_hours?.[index],
precipitationProbability: daily.precipitation_probability_max?.[index],
precipitationSum: daily.precipitation_sum?.[index],
sunrise: daily.sunrise[index],
sunset: daily.sunset[index],
sunrise: new Date(daily.sunrise[0] || 0).toLocaleTimeString(),
sunset: new Date(daily.sunset[0] || 0).toLocaleTimeString(),
sunshineDuration: daily.sunshine_duration?.[index],
temperatureMax: daily.temperature_2m_max?.[index],
temperatureMin: daily.temperature_2m_min?.[index],
@@ -108,6 +72,7 @@ export default function ExtendedForecastCard({ daily }: { daily: Daily | undefin
style={{
borderRadius: '8px',
}}
onClick={() => setSelectedDay(item)}
>
<Group
justify={

View File

@@ -0,0 +1,244 @@
import { Box, Card, Group, Stack, Text } from '@mantine/core';
import { FaArrowDown, FaArrowUp, FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { WiRain, WiSunrise, WiSunset } from 'react-icons/wi';
import { WeatherCodes } from 'types';
import WeatherFeaturePill from './WeatherFeaturePill';
import WeatherIcon from './WeatherIcon';
export default function ExtendedForecastDetails({ selectedDay }: { selectedDay: any }) {
return (
<Card
h={'100%'}
w={'100%'}
style={{
color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-background-0)',
padding: '1rem',
borderRadius: '8px',
}}
onClick={(e) => {
e.stopPropagation();
}}
shadow='xl'
p={0}
>
<Group
w={'100%'}
justify='center'
bg={'var(--mantine-color-search-0)'}
p={0}
mt={0}
>
<Text fz={'1.5rem'}>
{selectedDay.time.toLocaleDateString('en-US', { weekday: 'long' })}
</Text>
<Text fz={'1.5rem'}>{selectedDay.time.toLocaleDateString()}</Text>
</Group>
<Stack align='center' p={16}>
<WeatherIcon size={'xl'} code={selectedDay.weatherCode} />
<Text fz={'1rem'} fw={600} mt={'-32px'}>
{WeatherCodes[Number(selectedDay.weatherCode)]}
</Text>
<Group>
<Group justify='space-between'>
<Stack gap={4}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<FaWind size={16} />
<Text>Wind</Text>
</Group>
}
rightSide={<Text>{selectedDay.windSpeed?.toFixed(0)}mph</Text>}
highlight='right'
/>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<FaWind size={16} />
<Text>Gusts</Text>
</Group>
}
rightSide={<Text>{selectedDay.windGusts?.toFixed(0)}mph</Text>}
highlight='right'
/>
</Stack>
<Stack align='center' gap={0}>
<Box
style={{
border: '2px solid var(--mantine-color-primary-0)',
borderRadius: '100%',
aspectRatio: 1,
width: 'fit-content',
padding: '.75rem',
display: 'flex',
backgroundColor: 'var(--mantine-color-background-0)',
}}
>
<FaLocationArrow
style={{
color: 'var(--mantine-color-accent-0)',
transform: `rotate(${-48 + (selectedDay.windDirection || 0)}deg)`,
}}
/>
</Box>
<Text fz={12}>{selectedDay.windDirection?.toFixed(0)}°</Text>
</Stack>
</Group>
<Group justify='space-between'>
<Stack align='center' justify='center' gap={0}>
<Box
style={{
border: '2px solid var(--mantine-color-primary-0)',
borderRadius: '100%',
aspectRatio: 1,
width: 'fit-content',
padding: '.75rem',
display: 'flex',
backgroundColor: 'var(--mantine-color-background-0)',
}}
>
<FaSun
style={{
color: 'var(--mantine-color-accent-0)',
}}
/>
</Box>
<Text fz={12}>UV {selectedDay.uvIndexMax?.toFixed(0)}</Text>
</Stack>
<Stack gap={4}>
<WeatherFeaturePill
leftSide={
<Group gap={0}>
<WiSunrise
size={18}
style={{
marginTop: '2px',
marginBottom: '2px',
marginLeft: -6,
marginRight: -6,
}}
/>
</Group>
}
rightSide={<Text>{selectedDay.sunrise}</Text>}
highlight='left'
/>
<WeatherFeaturePill
leftSide={
<Group gap={0}>
<WiSunset
size={18}
style={{
marginTop: '2px',
marginBottom: '2px',
marginLeft: -6,
marginRight: -6,
}}
/>
</Group>
}
rightSide={<Text>{selectedDay.sunset}</Text>}
highlight='left'
/>
</Stack>
</Group>
</Group>
<Group w={'100%'} gap={4} justify='space-between'>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<FaArrowUp size={16} />
<Text>High Temp</Text>
</Group>
}
rightSide={
<Text>
{selectedDay.temperatureMax.toFixed(0)}°<sup>F</sup>
</Text>
}
highlight='right'
/>
</Box>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<FaArrowDown size={16} />
<Text>Low Temp</Text>
</Group>
}
rightSide={
<Text>
{selectedDay.temperatureMin.toFixed(0)}°<sup>F</sup>
</Text>
}
highlight='right'
/>
</Box>
</Group>
<Group w={'100%'} gap={4} justify='space-between'>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<FaArrowUp size={16} />
<Text>Feels Like High</Text>
</Group>
}
rightSide={
<Text>
{selectedDay.apparentTemperatureMax.toFixed(0)}°<sup>F</sup>
</Text>
}
highlight='right'
/>
</Box>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<FaArrowDown size={16} />
<Text>Feels Like Temp</Text>
</Group>
}
rightSide={
<Text>
{selectedDay.apparentTemperatureMin.toFixed(0)}°<sup>F</sup>
</Text>
}
highlight='right'
/>
</Box>
</Group>
<Group w={'100%'} gap={4} justify='space-between'>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<WiRain size={16} />
<Text>Precipitation %</Text>
</Group>
}
rightSide={<Text>{selectedDay.precipitationProbability}%</Text>}
highlight='right'
/>
</Box>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
<WiRain size={16} />
<Text>Precipitation In.</Text>
</Group>
}
rightSide={<Text>{selectedDay.precipitationSum.toFixed(2)} in.</Text>}
highlight='right'
/>
</Box>
</Group>
</Stack>
</Card>
);
}

View File

@@ -1,4 +1,5 @@
import { Stack } from '@mantine/core';
import { Group, Select, Stack } from '@mantine/core';
import { useState } from 'react';
import {
Area,
AreaChart,
@@ -11,16 +12,18 @@ import {
XAxis,
YAxis,
} from 'recharts';
import { selectClasses } from 'styles';
import type { Hourly } from 'types';
import WeatherTooltip from './WeatherTooltip';
export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }) {
const colorPrecipitation = 'colorPrecipitation';
const colorHumidity = 'colorHumidity';
const [timeFrame, setTimeFrame] = useState(24);
const later = new Date();
const now = new Date();
later.setDate(now.getDate() + 1);
later.setTime(now.getTime() + timeFrame * 60 * 60 * 1000);
const hourlyForecast = hourly?.time
?.map((_: any, index: number) => {
return {
@@ -31,6 +34,7 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
endTime: hourly.time[index + 1],
probabilityOfPrecipitation: hourly.precipitation_probability?.[index],
temperature: hourly.temperature_2m?.[index].toFixed(0),
apparentTemperature: hourly.apparent_temperature?.[index].toFixed(0),
relativeHumidity: hourly.relative_humidity_2m?.[index].toFixed(0),
};
})
@@ -42,8 +46,32 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
background: 'var(--mantine-color-search-0)',
padding: '.5rem',
borderRadius: '8px',
position: 'relative',
marginTop: '.5rem',
}}
>
<Group m={0} p={0} mt={'-1.5rem'}>
<Select
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
dropdown: selectClasses.dropdown,
}}
className={selectClasses.menuSelect}
defaultValue={24}
value={timeFrame}
data={[
{ value: 12, label: '12 hours' },
{ value: 24, label: '24 hours' },
{ value: 28, label: '2 days' },
{ value: 48, label: '3 days' },
{ value: 168, label: '7 days' },
]}
onChange={(value: any) => setTimeFrame(value)}
/>
</Group>
<ResponsiveContainer width={'100%'} height={150}>
<ComposedChart data={hourlyForecast}>
<XAxis
@@ -81,8 +109,8 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
<Line
unit={'°F'}
type='monotone'
name='Dew Point'
dataKey='dewpoint'
name='Feels Like'
dataKey='apparentTemperature'
stroke={'var(--mantine-color-accent-0)'}
strokeWidth={2}
dot={{

View File

@@ -1,9 +1,11 @@
import { Center, Group, Modal, Stack, Text } from '@mantine/core';
import { Group, Modal, Overlay, Stack, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { useState } from 'react';
import { modalClasses } from 'styles';
import type { MeteoWeather } from 'types';
import CurrentWeather from './CurrentWeather';
import ExtendedForecastCard from './ExtendedForecastCard';
import ExtendedForecastDetails from './ExtendedForecastDetails';
import WeatherGraphs from './WeatherGraphs';
export function WeatherModal({
@@ -18,7 +20,7 @@ export function WeatherModal({
closeWeatherModal: () => void;
}) {
const { isMobile, currentUser } = useCommonHooks();
const [selectedDay, setSelectedDay] = useState<any>(undefined);
return (
<Modal.Root
fullScreen={isMobile}
@@ -30,6 +32,7 @@ export function WeatherModal({
onClose={() => {
handleResetSearch();
closeWeatherModal();
setSelectedDay(undefined);
}}
closeOnEscape
closeOnClickOutside
@@ -54,13 +57,32 @@ export function WeatherModal({
<Modal.CloseButton />
</Modal.Header>
<Modal.Body m={0}>
<Center w={'100%'}>
<Stack w={'100%'}>
<CurrentWeather current={current} />
<WeatherGraphs hourly={current?.hourly} />
<ExtendedForecastCard daily={current?.daily} />
</Stack>
</Center>
<Stack w={'100%'} h={'100%'} pos={'relative'}>
{selectedDay && (
<Overlay
h={'100%'}
opacity={1}
blur={10}
onClick={() => setSelectedDay(undefined)}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'color-mix(in srgb, black, transparent 50%)',
}}
>
<Stack w={'65%'} align={'center'} justify={'center'}>
<ExtendedForecastDetails selectedDay={selectedDay} />
</Stack>
</Overlay>
)}
<CurrentWeather current={current} />
<WeatherGraphs hourly={current?.hourly} />
<ExtendedForecastCard
daily={current?.daily}
setSelectedDay={setSelectedDay}
/>
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>

View File

@@ -16,4 +16,5 @@ export type {
MeteoWeather,
SunState,
} from './weather';
export { WeatherCodes } from './weatherIcon';
export type { WolClient } from './wolClient';

41
src/types/weatherIcon.ts Normal file
View File

@@ -0,0 +1,41 @@
export const WeatherCodes: { [key: number]: string } = {
0: 'Clear sky',
1: 'Mainly clear',
2: 'Partly cloudy',
3: 'Overcast',
45: 'Fog',
48: 'Depositing rime fog',
51: 'Drizzle: Light',
53: 'Drizzle: Moderate',
55: 'Drizzle: Dense',
56: 'Freezing Drizzle: Light',
57: 'Freezing Drizzle: Dense',
61: 'Rain: Slight',
63: 'Rain: Moderate',
65: 'Rain: Heavy',
66: 'Freezing Rain: Light',
67: 'Freezing Rain: Heavy',
71: 'Snowfall: Slight',
73: 'Snowfall: Moderate',
75: 'Snowfall: Heavy',
77: 'Snow grains',
80: 'Rain showers: Slight',
81: 'Rain showers: Moderate',
82: 'Rain showers: Violent',
85: 'Snow showers: Slight',
86: 'Snow showers: Heavy',
95: 'Thunderstorm: Slight or moderate',
96: 'Thunderstorm with slight hail',
99: 'Thunderstorm with heavy hail',
} as const;