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

View File

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

View File

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

View File

@@ -1,18 +1,26 @@
import { ActionIcon, Box, Card, Group, Stack, Text } from '@mantine/core';
import { FaArrowDown, FaArrowUp, FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { FaX } from 'react-icons/fa6';
import { useCommonHooks } from 'hooks';
import { FaLocationArrow, FaSun, FaWind } from 'react-icons/fa';
import { FaDroplet, FaX } from 'react-icons/fa6';
import { WiRain, WiSunrise, WiSunset } from 'react-icons/wi';
import { textClasses } from 'styles';
import { WeatherCodes } from 'types';
import WeatherFeaturePill from './WeatherFeaturePill';
import WeatherGraphs from './WeatherGraphs';
import WeatherIcon from './WeatherIcon';
export default function ExtendedForecastDetails({
selectedDay,
setSelectedDay,
selectedHourly,
setSelectedHourly,
}: {
selectedDay: any;
setSelectedDay: (day: any) => void;
selectedHourly: any;
setSelectedHourly: (hourly: any) => void;
}) {
const { isMobile } = useCommonHooks();
return (
<Card
h={'100%'}
@@ -24,7 +32,8 @@ export default function ExtendedForecastDetails({
borderRadius: '8px',
overflow: 'visible',
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) => {
e.stopPropagation();
@@ -41,6 +50,7 @@ export default function ExtendedForecastDetails({
}}
onClick={() => {
setSelectedDay(undefined);
setSelectedHourly(undefined);
}}
>
<FaX />
@@ -64,10 +74,96 @@ export default function ExtendedForecastDetails({
</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>
<Stack
gap={0}
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 justify='space-between'>
<Stack gap={4}>
@@ -173,76 +269,27 @@ export default function ExtendedForecastDetails({
</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%'}>
<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)',
}}
>
<FaDroplet
style={{
color: 'var(--mantine-color-accent-0)',
}}
/>
</Box>
</Stack>
<Stack gap={4}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
@@ -253,8 +300,6 @@ export default function ExtendedForecastDetails({
rightSide={<Text>{selectedDay.precipitationProbability}%</Text>}
highlight='right'
/>
</Box>
<Box w={'48%'}>
<WeatherFeaturePill
leftSide={
<Group gap={2}>
@@ -265,8 +310,9 @@ export default function ExtendedForecastDetails({
rightSide={<Text>{selectedDay.precipitationSum.toFixed(2)} in.</Text>}
highlight='right'
/>
</Box>
</Stack>
</Group>
<WeatherGraphs hourly={selectedHourly} formatted />
</Stack>
</Card>
);

View File

@@ -13,34 +13,42 @@ import {
YAxis,
} from 'recharts';
import { selectClasses } from 'styles';
import type { Hourly } from 'types';
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 colorHumidity = 'colorHumidity';
const [timeFrame, setTimeFrame] = useState(24);
const later = new Date();
const now = new Date();
later.setTime(now.getTime() + timeFrame * 60 * 60 * 1000);
const hourlyForecast = hourly?.time
?.map((_: any, index: number) => {
return {
dewpoint: hourly.dew_point_2m?.[index].toFixed(0),
isDaytime: hourly.is_day?.[index],
name: hourly.time[index].toLocaleString('en-us'),
startTime: hourly.time[index],
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),
};
})
.filter((hour: any) => hour.startTime > now && hour.startTime < later);
const hourlyForecast = formatted
? hourly
: hourly?.time
?.map((_: any, index: number) => {
return {
dewpoint: hourly.dew_point_2m?.[index].toFixed(0),
isDaytime: hourly.is_day?.[index],
name: hourly.time[index].toLocaleString('en-us'),
startTime: hourly.time[index],
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),
};
})
.filter((hour: any) => hour.startTime > now && hour.startTime < later);
return (
<Stack
w={'100%'}
style={{
background: 'var(--mantine-color-search-0)',
padding: '.5rem',
@@ -49,28 +57,30 @@ export default function WeatherGraphs({ hourly }: { hourly: Hourly | undefined }
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: 48, label: '2 days' },
{ value: 72, label: '3 days' },
{ value: 168, label: '7 days' },
]}
onChange={(value: any) => setTimeFrame(value)}
/>
</Group>
{!formatted && (
<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: 48, label: '2 days' },
{ value: 72, label: '3 days' },
{ value: 168, label: '7 days' },
]}
onChange={(value: any) => setTimeFrame(value)}
/>
</Group>
)}
<ResponsiveContainer width={'100%'} height={150}>
<ComposedChart data={hourlyForecast}>
<XAxis

View File

@@ -1,6 +1,6 @@
import { Group, Modal, Overlay, Stack, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { modalClasses } from 'styles';
import type { MeteoWeather } from 'types';
import CurrentWeather from './CurrentWeather';
@@ -21,6 +21,35 @@ export function WeatherModal({
}) {
const { isMobile, currentUser } = useCommonHooks();
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 (
<Modal.Root
fullScreen={isMobile}
@@ -80,6 +109,8 @@ export function WeatherModal({
<ExtendedForecastDetails
selectedDay={selectedDay}
setSelectedDay={setSelectedDay}
selectedHourly={selectedHourly}
setSelectedHourly={setSelectedHourly}
/>
</Stack>
</Overlay>