Updates to details
All checks were successful
Deploy to production / build (push) Successful in 18s

This commit is contained in:
Sean Edmunds
2026-07-16 11:26:16 -05:00
parent 0685488252
commit 001843dcd2
7 changed files with 237 additions and 134 deletions

35
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"@tanstack/react-query-persist-client": "^5.101.2", "@tanstack/react-query-persist-client": "^5.101.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"jotai": "^2.20.1", "jotai": "^2.20.2",
"openmeteo": "^1.2.3", "openmeteo": "^1.2.3",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
@@ -38,7 +38,7 @@
"@vitest/coverage-v8": "^4.1.10", "@vitest/coverage-v8": "^4.1.10",
"@vitest/ui": "^4.1.10", "@vitest/ui": "^4.1.10",
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.1.4", "vite": "^8.1.5",
"vite-plugin-dts": "~5.0.3", "vite-plugin-dts": "~5.0.3",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.10" "vitest": "^4.1.10"
@@ -1918,9 +1918,9 @@
} }
}, },
"node_modules/jotai": { "node_modules/jotai": {
"version": "2.20.1", "version": "2.20.2",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.20.1.tgz", "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.20.2.tgz",
"integrity": "sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg==", "integrity": "sha512-aHB4CNb9qRcyf0mwSB6EO5bCGAjx8cTwFgOFCE2leOnTzqACbnSWG8XoWB3LxCT1Qoj03I1OWAHszDmN4uHb/w==",
"engines": { "engines": {
"node": ">=12.20.0" "node": ">=12.20.0"
}, },
@@ -3097,15 +3097,15 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.1.4", "version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.5", "picomatch": "^4.0.5",
"postcss": "^8.5.16", "postcss": "^8.5.17",
"rolldown": "~1.1.4", "rolldown": "~1.1.5",
"tinyglobby": "^0.2.17" "tinyglobby": "^0.2.17"
}, },
"bin": { "bin": {
@@ -3233,6 +3233,21 @@
} }
} }
}, },
"node_modules/vite-tsconfig-paths/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vitest": { "node_modules/vitest": {
"version": "4.1.10", "version": "4.1.10",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",

View File

@@ -23,7 +23,7 @@
"@tanstack/react-query-persist-client": "^5.101.2", "@tanstack/react-query-persist-client": "^5.101.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"jotai": "^2.20.1", "jotai": "^2.20.2",
"openmeteo": "^1.2.3", "openmeteo": "^1.2.3",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
@@ -40,7 +40,7 @@
"@vitest/coverage-v8": "^4.1.10", "@vitest/coverage-v8": "^4.1.10",
"@vitest/ui": "^4.1.10", "@vitest/ui": "^4.1.10",
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.1.4", "vite": "^8.1.5",
"vite-plugin-dts": "~5.0.3", "vite-plugin-dts": "~5.0.3",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.10" "vitest": "^4.1.10"

View File

@@ -17,7 +17,7 @@ import { datePickerInputClasses, selectClasses } from 'styles';
declare module '@tanstack/react-table' { declare module '@tanstack/react-table' {
//allows us to define custom properties for our columns //allows us to define custom properties for our columns
interface ColumnMeta<TData extends RowData, TValue> { interface ColumnMeta<_TData extends RowData, _TValue> {
filterVariant?: filterVariant?:
| 'text' | 'text'
| 'range' | 'range'

View File

@@ -230,6 +230,7 @@ export default function CurrentWeather({
? textClasses.autoScrollText ? textClasses.autoScrollText
: undefined : undefined
} }
fz={isMobile ? 12 : 18}
classNames={textClasses} classNames={textClasses}
style={{ lineHeight: 1 }} style={{ lineHeight: 1 }}
> >

View File

@@ -1,18 +1,26 @@
import { ActionIcon, Box, Card, Group, Stack, Text } from '@mantine/core'; import { ActionIcon, Box, Card, Group, Stack, Text } from '@mantine/core';
import { FaArrowDown, FaArrowUp, FaLocationArrow, FaSun, FaWind } from 'react-icons/fa'; import { useCommonHooks } from 'hooks';
import { FaX } from 'react-icons/fa6'; import { FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { FaDroplet, FaX } from 'react-icons/fa6';
import { WiRain, WiSunrise, WiSunset } from 'react-icons/wi'; import { WiRain, WiSunrise, WiSunset } from 'react-icons/wi';
import { textClasses } from 'styles';
import { WeatherCodes } from 'types'; import { WeatherCodes } from 'types';
import WeatherFeaturePill from './WeatherFeaturePill'; import WeatherFeaturePill from './WeatherFeaturePill';
import WeatherGraphs from './WeatherGraphs';
import WeatherIcon from './WeatherIcon'; import WeatherIcon from './WeatherIcon';
export default function ExtendedForecastDetails({ export default function ExtendedForecastDetails({
selectedDay, selectedDay,
setSelectedDay, setSelectedDay,
selectedHourly,
setSelectedHourly,
}: { }: {
selectedDay: any; selectedDay: any;
setSelectedDay: (day: any) => void; setSelectedDay: (day: any) => void;
selectedHourly: any;
setSelectedHourly: (hourly: any) => void;
}) { }) {
const { isMobile } = useCommonHooks();
return ( return (
<Card <Card
h={'100%'} h={'100%'}
@@ -24,7 +32,8 @@ export default function ExtendedForecastDetails({
borderRadius: '8px', borderRadius: '8px',
overflow: 'visible', overflow: 'visible',
cursor: 'default', cursor: 'default',
boxShadow: '15px 15px 3px var(--mantine-color-border-0)', boxShadow:
'15px 15px 2px color-mix(in srgb, var(--mantine-color-border-0), transparent 50%)',
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -41,6 +50,7 @@ export default function ExtendedForecastDetails({
}} }}
onClick={() => { onClick={() => {
setSelectedDay(undefined); setSelectedDay(undefined);
setSelectedHourly(undefined);
}} }}
> >
<FaX /> <FaX />
@@ -64,10 +74,96 @@ export default function ExtendedForecastDetails({
</Text> </Text>
</Group> </Group>
<Stack align='center' p={16}> <Stack align='center' p={16}>
<WeatherIcon size={'xl'} code={selectedDay.weatherCode} /> <Group>
<Text fz={'1rem'} fw={600} mt={'-32px'}> <Stack
{WeatherCodes[Number(selectedDay.weatherCode)]} gap={0}
</Text> style={{
overflow: 'hidden',
backgroundColor: 'var(--mantine-color-background-0)',
border: 'thin solid var(--mantine-color-primary-0)',
zIndex: 1,
paddingLeft: '2.25rem',
paddingRight: '2.25rem',
borderRadius: '100%',
aspectRatio: 1,
}}
align='center'
justify='center'
>
<Box m={0} p={0}>
<WeatherFeaturePill
leftSide={<Text>Feels like</Text>}
rightSide={
<Text>
{selectedDay.apparentTemperatureMax?.toFixed(0) ??
selectedDay.temperatureMax?.toFixed(0)}
°<sup>F</sup>
</Text>
}
highlight='right'
/>
</Box>
<Group pos={'relative'}>
<Text fz={isMobile ? '5rem' : '7rem'} fw={400} style={{ lineHeight: 1 }}>
{selectedDay.temperatureMax?.toFixed(0)}
<sup style={{ fontSize: '3rem' }}>°F</sup>
</Text>
<Text style={{ lineHeight: 1 }} pos={'absolute'} bottom={20} right={0}>
/ {selectedDay.temperatureMin?.toFixed(0)}°<sup>F</sup>
</Text>
<Box
style={{
position: 'absolute',
backgroundColor:
'color-mix(in srgb, var(--mantine-color-accent-0), transparent 80%)',
height: '250%',
width: '80%',
bottom: -96,
right: -60,
transform: 'skewX(-25deg)',
zIndex: -1,
borderRadius: '2px',
backdropFilter: 'brightness(0.5)',
lineHeight: 1,
}}
/>
</Group>
<Group
justify={
WeatherCodes[Number(selectedDay.weatherCode)].length > 25
? 'flex-start'
: 'center'
}
style={{
width: '100%',
maxWidth: '180px',
whiteSpace: 'nowrap',
overflow: 'hidden',
boxSizing: 'border-box',
position: 'relative',
}}
>
<Text
className={
WeatherCodes[Number(selectedDay.weatherCode)].length > 25
? textClasses.autoScrollText
: undefined
}
classNames={textClasses}
style={{ lineHeight: 1 }}
>
{selectedDay.temperature_2m?.toFixed(0) === '69'
? 'Nice'
: WeatherCodes[Number(selectedDay.weatherCode)]}
</Text>
</Group>
</Stack>
<Stack>
<WeatherIcon size={'xl'} code={selectedDay.weatherCode} />
</Stack>
</Group>
<Group> <Group>
<Group justify='space-between'> <Group justify='space-between'>
<Stack gap={4}> <Stack gap={4}>
@@ -173,76 +269,27 @@ export default function ExtendedForecastDetails({
</Stack> </Stack>
</Group> </Group>
</Group> </Group>
<Group w={'100%'} gap={4} justify='space-between'> <Group justify='space-between'>
<Box w={'48%'}> <Stack align='center' justify='center' gap={0}>
<WeatherFeaturePill <Box
leftSide={ style={{
<Group gap={2}> border: '2px solid var(--mantine-color-primary-0)',
<FaArrowUp size={16} /> borderRadius: '100%',
<Text>High Temp</Text> aspectRatio: 1,
</Group> width: 'fit-content',
} padding: '.75rem',
rightSide={ display: 'flex',
<Text> backgroundColor: 'var(--mantine-color-background-0)',
{selectedDay.temperatureMax.toFixed(0)}°<sup>F</sup> }}
</Text> >
} <FaDroplet
highlight='right' style={{
/> color: 'var(--mantine-color-accent-0)',
</Box> }}
<Box w={'48%'}> />
<WeatherFeaturePill </Box>
leftSide={ </Stack>
<Group gap={2}> <Stack gap={4}>
<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 <WeatherFeaturePill
leftSide={ leftSide={
<Group gap={2}> <Group gap={2}>
@@ -253,8 +300,6 @@ export default function ExtendedForecastDetails({
rightSide={<Text>{selectedDay.precipitationProbability}%</Text>} rightSide={<Text>{selectedDay.precipitationProbability}%</Text>}
highlight='right' highlight='right'
/> />
</Box>
<Box w={'48%'}>
<WeatherFeaturePill <WeatherFeaturePill
leftSide={ leftSide={
<Group gap={2}> <Group gap={2}>
@@ -265,8 +310,9 @@ export default function ExtendedForecastDetails({
rightSide={<Text>{selectedDay.precipitationSum.toFixed(2)} in.</Text>} rightSide={<Text>{selectedDay.precipitationSum.toFixed(2)} in.</Text>}
highlight='right' highlight='right'
/> />
</Box> </Stack>
</Group> </Group>
<WeatherGraphs hourly={selectedHourly} formatted />
</Stack> </Stack>
</Card> </Card>
); );

View File

@@ -13,34 +13,42 @@ import {
YAxis, YAxis,
} from 'recharts'; } from 'recharts';
import { selectClasses } from 'styles'; import { selectClasses } from 'styles';
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,
formatted,
}: {
hourly: any;
formatted?: boolean;
}) {
const colorPrecipitation = 'colorPrecipitation'; const colorPrecipitation = 'colorPrecipitation';
const colorHumidity = 'colorHumidity'; const colorHumidity = 'colorHumidity';
const [timeFrame, setTimeFrame] = useState(24); const [timeFrame, setTimeFrame] = useState(24);
const later = new Date(); const later = new Date();
const now = new Date(); const now = new Date();
later.setTime(now.getTime() + timeFrame * 60 * 60 * 1000); later.setTime(now.getTime() + timeFrame * 60 * 60 * 1000);
const hourlyForecast = hourly?.time const hourlyForecast = formatted
?.map((_: any, index: number) => { ? hourly
return { : hourly?.time
dewpoint: hourly.dew_point_2m?.[index].toFixed(0), ?.map((_: any, index: number) => {
isDaytime: hourly.is_day?.[index], return {
name: hourly.time[index].toLocaleString('en-us'), dewpoint: hourly.dew_point_2m?.[index].toFixed(0),
startTime: hourly.time[index], isDaytime: hourly.is_day?.[index],
endTime: hourly.time[index + 1], name: hourly.time[index].toLocaleString('en-us'),
probabilityOfPrecipitation: hourly.precipitation_probability?.[index], startTime: hourly.time[index],
temperature: hourly.temperature_2m?.[index].toFixed(0), endTime: hourly.time[index + 1],
apparentTemperature: hourly.apparent_temperature?.[index].toFixed(0), probabilityOfPrecipitation: hourly.precipitation_probability?.[index],
relativeHumidity: hourly.relative_humidity_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),
.filter((hour: any) => hour.startTime > now && hour.startTime < later); };
})
.filter((hour: any) => hour.startTime > now && hour.startTime < later);
return ( return (
<Stack <Stack
w={'100%'}
style={{ style={{
background: 'var(--mantine-color-search-0)', background: 'var(--mantine-color-search-0)',
padding: '.5rem', padding: '.5rem',
@@ -49,28 +57,30 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
marginTop: '.5rem', marginTop: '.5rem',
}} }}
> >
<Group m={0} p={0} mt={'-1.5rem'}> {!formatted && (
<Select <Group m={0} p={0} mt={'-1.5rem'}>
classNames={{ <Select
input: selectClasses.menuSelectInput, classNames={{
label: selectClasses.menuSelectLabel, input: selectClasses.menuSelectInput,
options: selectClasses.menuSelectOptions, label: selectClasses.menuSelectLabel,
option: selectClasses.menuSelectOption, options: selectClasses.menuSelectOptions,
dropdown: selectClasses.dropdown, option: selectClasses.menuSelectOption,
}} dropdown: selectClasses.dropdown,
className={selectClasses.menuSelect} }}
defaultValue={24} className={selectClasses.menuSelect}
value={timeFrame} defaultValue={24}
data={[ value={timeFrame}
{ value: 12, label: '12 hours' }, data={[
{ value: 24, label: '24 hours' }, { value: 12, label: '12 hours' },
{ value: 48, label: '2 days' }, { value: 24, label: '24 hours' },
{ value: 72, label: '3 days' }, { value: 48, label: '2 days' },
{ value: 168, label: '7 days' }, { value: 72, label: '3 days' },
]} { value: 168, label: '7 days' },
onChange={(value: any) => setTimeFrame(value)} ]}
/> onChange={(value: any) => setTimeFrame(value)}
</Group> />
</Group>
)}
<ResponsiveContainer width={'100%'} height={150}> <ResponsiveContainer width={'100%'} height={150}>
<ComposedChart data={hourlyForecast}> <ComposedChart data={hourlyForecast}>
<XAxis <XAxis

View File

@@ -1,6 +1,6 @@
import { Group, Modal, Overlay, 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 { useEffect, 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';
@@ -21,6 +21,35 @@ export function WeatherModal({
}) { }) {
const { isMobile, currentUser } = useCommonHooks(); const { isMobile, currentUser } = useCommonHooks();
const [selectedDay, setSelectedDay] = useState<any>(undefined); const [selectedDay, setSelectedDay] = useState<any>(undefined);
const [selectedHourly, setSelectedHourly] = useState<any>(undefined);
useEffect(() => {
if (selectedDay && current) {
const later = new Date().setTime(selectedDay.time.getTime() + 24 * 60 * 60 * 1000);
setSelectedHourly(
current?.hourly.time
?.map((_: any, index: number) => {
return {
dewpoint: current?.hourly.dew_point_2m?.[index].toFixed(0),
isDaytime: current?.hourly.is_day?.[index],
name: current?.hourly.time[index].toLocaleString('en-us'),
startTime: current?.hourly.time[index],
endTime: current?.hourly.time[index + 1],
probabilityOfPrecipitation:
current?.hourly.precipitation_probability?.[index],
temperature: current?.hourly.temperature_2m?.[index].toFixed(0),
apparentTemperature:
current?.hourly.apparent_temperature?.[index].toFixed(0),
relativeHumidity: current?.hourly.relative_humidity_2m?.[index].toFixed(0),
};
})
.filter(
(hour: any) => hour.startTime >= selectedDay.time && hour.startTime < later,
),
);
}
}, [selectedDay]);
return ( return (
<Modal.Root <Modal.Root
fullScreen={isMobile} fullScreen={isMobile}
@@ -80,6 +109,8 @@ export function WeatherModal({
<ExtendedForecastDetails <ExtendedForecastDetails
selectedDay={selectedDay} selectedDay={selectedDay}
setSelectedDay={setSelectedDay} setSelectedDay={setSelectedDay}
selectedHourly={selectedHourly}
setSelectedHourly={setSelectedHourly}
/> />
</Stack> </Stack>
</Overlay> </Overlay>