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 { FaCloud, FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { WiBarometer, WiHumidity, WiRain, WiSunrise, WiSunset } from 'react-icons/wi'; import { WiBarometer, WiHumidity, WiRain, WiSunrise, WiSunset } from 'react-icons/wi';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
import type { MeteoWeather } from 'types'; import { type MeteoWeather, WeatherCodes } from 'types';
import WeatherFeaturePill from './WeatherFeaturePill'; import WeatherFeaturePill from './WeatherFeaturePill';
import WeatherIcon from './WeatherIcon'; 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({ export default function CurrentWeather({
current, current,
}: { }: {
@@ -321,7 +279,6 @@ export default function CurrentWeather({
<FaSun <FaSun
style={{ style={{
color: 'var(--mantine-color-accent-0)', color: 'var(--mantine-color-accent-0)',
transform: `rotate(${-45 + (current.current.wind_direction_10m || 0)}deg)`,
}} }}
/> />
</Box> </Box>

View File

@@ -1,52 +1,16 @@
import { Card, Group, SimpleGrid, Stack, Text } from '@mantine/core'; import { Card, Group, SimpleGrid, Stack, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
import type { Daily } from 'types'; import { type Daily, WeatherCodes } from 'types';
import WeatherIcon from './WeatherIcon'; import WeatherIcon from './WeatherIcon';
const WeatherCodes: { [key: number]: string } = { export default function ExtendedForecastCard({
0: 'Clear sky', daily,
setSelectedDay,
1: 'Mainly clear', }: {
2: 'Partly cloudy', daily: Daily | undefined;
3: 'Overcast', setSelectedDay: any;
}) {
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 }) {
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme } = useCommonHooks();
const forecast = daily?.time.map((_: any, index: number) => { const forecast = daily?.time.map((_: any, index: number) => {
const time = new Date(daily.time[index]); const time = new Date(daily.time[index]);
@@ -59,8 +23,8 @@ export default function ExtendedForecastCard({ daily }: { daily: Daily | undefin
precipitationHours: daily.precipitation_hours?.[index], precipitationHours: daily.precipitation_hours?.[index],
precipitationProbability: daily.precipitation_probability_max?.[index], precipitationProbability: daily.precipitation_probability_max?.[index],
precipitationSum: daily.precipitation_sum?.[index], precipitationSum: daily.precipitation_sum?.[index],
sunrise: daily.sunrise[index], sunrise: new Date(daily.sunrise[0] || 0).toLocaleTimeString(),
sunset: daily.sunset[index], sunset: new Date(daily.sunset[0] || 0).toLocaleTimeString(),
sunshineDuration: daily.sunshine_duration?.[index], sunshineDuration: daily.sunshine_duration?.[index],
temperatureMax: daily.temperature_2m_max?.[index], temperatureMax: daily.temperature_2m_max?.[index],
temperatureMin: daily.temperature_2m_min?.[index], temperatureMin: daily.temperature_2m_min?.[index],
@@ -108,6 +72,7 @@ export default function ExtendedForecastCard({ daily }: { daily: Daily | undefin
style={{ style={{
borderRadius: '8px', borderRadius: '8px',
}} }}
onClick={() => setSelectedDay(item)}
> >
<Group <Group
justify={ 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 { import {
Area, Area,
AreaChart, AreaChart,
@@ -11,16 +12,18 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import { selectClasses } from 'styles';
import type { Hourly } from 'types'; import type { Hourly } from 'types';
import WeatherTooltip from './WeatherTooltip'; import WeatherTooltip from './WeatherTooltip';
export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }) { export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }) {
const colorPrecipitation = 'colorPrecipitation'; const colorPrecipitation = 'colorPrecipitation';
const colorHumidity = 'colorHumidity'; const colorHumidity = 'colorHumidity';
const [timeFrame, setTimeFrame] = useState(24);
const later = new Date(); const later = new Date();
const now = new Date(); const now = new Date();
later.setDate(now.getDate() + 1);
later.setTime(now.getTime() + timeFrame * 60 * 60 * 1000);
const hourlyForecast = hourly?.time const hourlyForecast = hourly?.time
?.map((_: any, index: number) => { ?.map((_: any, index: number) => {
return { return {
@@ -31,6 +34,7 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
endTime: hourly.time[index + 1], endTime: hourly.time[index + 1],
probabilityOfPrecipitation: hourly.precipitation_probability?.[index], probabilityOfPrecipitation: hourly.precipitation_probability?.[index],
temperature: hourly.temperature_2m?.[index].toFixed(0), temperature: hourly.temperature_2m?.[index].toFixed(0),
apparentTemperature: hourly.apparent_temperature?.[index].toFixed(0),
relativeHumidity: hourly.relative_humidity_2m?.[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)', background: 'var(--mantine-color-search-0)',
padding: '.5rem', padding: '.5rem',
borderRadius: '8px', 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}> <ResponsiveContainer width={'100%'} height={150}>
<ComposedChart data={hourlyForecast}> <ComposedChart data={hourlyForecast}>
<XAxis <XAxis
@@ -81,8 +109,8 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
<Line <Line
unit={'°F'} unit={'°F'}
type='monotone' type='monotone'
name='Dew Point' name='Feels Like'
dataKey='dewpoint' dataKey='apparentTemperature'
stroke={'var(--mantine-color-accent-0)'} stroke={'var(--mantine-color-accent-0)'}
strokeWidth={2} strokeWidth={2}
dot={{ 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 { useCommonHooks } from 'hooks';
import { useState } from 'react';
import { modalClasses } from 'styles'; import { modalClasses } from 'styles';
import type { MeteoWeather } from 'types'; import type { MeteoWeather } from 'types';
import CurrentWeather from './CurrentWeather'; import CurrentWeather from './CurrentWeather';
import ExtendedForecastCard from './ExtendedForecastCard'; import ExtendedForecastCard from './ExtendedForecastCard';
import ExtendedForecastDetails from './ExtendedForecastDetails';
import WeatherGraphs from './WeatherGraphs'; import WeatherGraphs from './WeatherGraphs';
export function WeatherModal({ export function WeatherModal({
@@ -18,7 +20,7 @@ export function WeatherModal({
closeWeatherModal: () => void; closeWeatherModal: () => void;
}) { }) {
const { isMobile, currentUser } = useCommonHooks(); const { isMobile, currentUser } = useCommonHooks();
const [selectedDay, setSelectedDay] = useState<any>(undefined);
return ( return (
<Modal.Root <Modal.Root
fullScreen={isMobile} fullScreen={isMobile}
@@ -30,6 +32,7 @@ export function WeatherModal({
onClose={() => { onClose={() => {
handleResetSearch(); handleResetSearch();
closeWeatherModal(); closeWeatherModal();
setSelectedDay(undefined);
}} }}
closeOnEscape closeOnEscape
closeOnClickOutside closeOnClickOutside
@@ -54,13 +57,32 @@ export function WeatherModal({
<Modal.CloseButton /> <Modal.CloseButton />
</Modal.Header> </Modal.Header>
<Modal.Body m={0}> <Modal.Body m={0}>
<Center w={'100%'}> <Stack w={'100%'} h={'100%'} pos={'relative'}>
<Stack w={'100%'}> {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} /> <CurrentWeather current={current} />
<WeatherGraphs hourly={current?.hourly} /> <WeatherGraphs hourly={current?.hourly} />
<ExtendedForecastCard daily={current?.daily} /> <ExtendedForecastCard
daily={current?.daily}
setSelectedDay={setSelectedDay}
/>
</Stack> </Stack>
</Center>
</Modal.Body> </Modal.Body>
</Modal.Content> </Modal.Content>
</Modal.Root> </Modal.Root>

View File

@@ -16,4 +16,5 @@ export type {
MeteoWeather, MeteoWeather,
SunState, SunState,
} from './weather'; } from './weather';
export { WeatherCodes } from './weatherIcon';
export type { WolClient } from './wolClient'; 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;