Hourly forecast

This commit is contained in:
Sean Edmunds
2024-12-05 14:36:11 -06:00
parent a6a96ba901
commit 407cff06c0
11 changed files with 329 additions and 127 deletions

View File

@@ -47,8 +47,14 @@ function App() {
const router = createBrowserRouter(
createRoutesFromElements(
<Route>
<Route path={`/${import.meta.env.VITE_BASE_URL}`} element={<Homepage theme={theme} />} />
<Route path={`/${import.meta.env.VITE_BASE_URL}/settings`} element={<Settings theme={theme} />} />
<Route
path={`/${import.meta.env.VITE_BASE_URL}`}
element={<Homepage theme={theme} />}
/>
<Route
path={`/${import.meta.env.VITE_BASE_URL}/settings`}
element={<Settings theme={theme} />}
/>
</Route>,
),
);

View File

@@ -45,6 +45,8 @@ export function useWeatherAPI({ endpoint }: { endpoint: string }) {
url = 'https://api.weather.gov/stations/KSGR/observations';
} else if (endpoint === 'forecast') {
url = 'https://api.weather.gov/gridpoints/HGX/55,96/forecast';
} else if (endpoint === 'hourly') {
url = 'https://api.weather.gov/gridpoints/HGX/55,96/forecast/hourly';
}
const callEndpoint = async (endpoint: string) => {
const options: object = {
@@ -158,7 +160,6 @@ export function useFileUploadAPI() {
endpoint: string,
body?: object | FormData | undefined | null,
) => {
const options = {
method: method,
headers: {

View File

@@ -1,13 +1,14 @@
import { Stack } from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { useDisclosure, useLocalStorage } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useQueryAPI } from '../../hooks/useAPI';
import { useQueryAPI, useWeatherAPI } from '../../hooks/useAPI';
import stackClasses from '../../styles/Stack.module.css';
import Applications from './components/Applications';
import Bookmarks from './components/Bookmarks';
import Header from './components/Header';
import Search from './components/Search';
import SettingsButton from './components/SettingsButton';
import type { CurrentWeather, OutlookWeather } from './types/weather';
export default function Homepage({ theme }: any) {
const { data, isFetching } = useQueryAPI({ endpoint: 'api/messages' });
@@ -20,6 +21,34 @@ export default function Homepage({ theme }: any) {
key: 'bookmarkFilter',
defaultValue: ['All'],
});
const { data: basicWeather } = useWeatherAPI({ endpoint: 'baseline' });
const { data: weather } = useWeatherAPI({ endpoint: 'current' });
const [current, setCurrent] = useState<CurrentWeather>({
icon: '',
temperature: 0,
highTemp: 0,
lowTemp: 0,
precipitation: 'N/A',
humidity: 0,
heatIndex: 0,
windSpeed: 0,
windDirection: 0,
});
const [outlook, setOutlook] = useState<OutlookWeather[]>([
{
name: '',
icon: '',
isDayTime: true,
temperature: 0,
precipitation: 0,
windSpeed: '0 mph',
windDirection: 'N',
},
]);
const { data: forecast } = useWeatherAPI({ endpoint: 'forecast' });
const { data: hourly } = useWeatherAPI({ endpoint: 'hourly' });
const [weatherModalOpened, { open: openWeatherModal, close: closeWeatherModal }] =
useDisclosure(false);
const isMobile = window.matchMedia('(max-width: 600px)').matches;
function date() {
const currentDate = new Date();
@@ -59,10 +88,90 @@ export default function Homepage({ theme }: any) {
}
}, [isFetching]);
useEffect(() => {
if (weather && basicWeather) {
setCurrent({
icon: weather.features[0].properties.icon?.replace('medium', 'large') || '',
temperature: Math.round(
(weather.features[0].properties.temperature.value * 9) / 5 + 32,
),
highTemp: Math.round(
(basicWeather.properties.maxTemperature.values[0].value * 9) / 5 + 32,
),
lowTemp: Math.round(
(basicWeather.properties.minTemperature.values[0].value * 9) / 5 + 32,
),
precipitation:
weather.features[0].properties.precipitationLastHour.value || 'N/A',
humidity: Math.round(weather.features[0].properties.relativeHumidity.value),
heatIndex: Math.round(
(weather.features[0].properties.heatIndex.value * 9) / 5 + 32,
),
windSpeed: Math.round(weather.features[0].properties.windSpeed.value / 1.60934),
windDirection: weather.features[0].properties.windDirection.value,
});
}
}, [weather, basicWeather]);
useEffect(() => {
if (forecast) {
setOutlook([
{
name: forecast.properties.periods[0].name,
icon: forecast.properties.periods[0].icon.replace('medium', 'large'),
isDayTime: forecast.properties.periods[0].isDaytime,
temperature: forecast.properties.periods[0].temperature,
precipitation: Math.round(
forecast.properties.periods[0].probabilityOfPrecipitation.value,
),
windSpeed: forecast.properties.periods[0].windSpeed,
windDirection: forecast.properties.periods[0].windDirection,
},
{
name: forecast.properties.periods[1].name,
icon: forecast.properties.periods[1].icon.replace('medium', 'large'),
isDayTime: forecast.properties.periods[1].isDaytime,
temperature: forecast.properties.periods[1].temperature,
precipitation: Math.round(
forecast.properties.periods[1].probabilityOfPrecipitation.value,
),
windSpeed: forecast.properties.periods[1].windSpeed,
windDirection: forecast.properties.periods[1].windDirection,
},
]);
}
}, [forecast]);
return (
<Stack gap={0} className={stackClasses.containerStack} justify='center'>
<Search />
<Header date={date()} greet={displayedGreeting} isMobile={isMobile} />
<Search
current={current}
setCurrent={setCurrent}
outlook={outlook}
setOutlook={setOutlook}
weather={weather}
basicWeather={basicWeather}
forecast={forecast}
hourly={hourly}
weatherModalOpened={weatherModalOpened}
closeWeatherModal={closeWeatherModal}
openWeatherModal={openWeatherModal}
/>
<Header
date={date()}
greet={displayedGreeting}
isMobile={isMobile}
current={current}
outlook={outlook}
setCurrent={setCurrent}
weather={weather}
basicWeather={basicWeather}
forecast={forecast}
hourly={hourly}
weatherModalOpened={weatherModalOpened}
closeWeatherModal={closeWeatherModal}
openWeatherModal={openWeatherModal}
/>
<Applications contentType={appFilter} isMobile={isMobile} />
<Bookmarks contentType={bookmarkFilter} isMobile={isMobile} />
<SettingsButton

View File

@@ -1,23 +1,26 @@
import { Group, Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import { useWeatherAPI } from '../../../hooks';
import { textClasses } from '../../../styles';
import WeatherModal from './WeatherModal';
function Header({ greet, date, isMobile }: any) {
function Header({
greet,
date,
isMobile,
current,
setCurrent,
weather,
basicWeather,
outlook,
hourly,
weatherModalOpened,
closeWeatherModal,
openWeatherModal,
setValue,
setProviderLabel,
setProviderURL,
}: any) {
const [time, setTime] = useState(new Date());
const { data: weather } = useWeatherAPI({ endpoint: 'current' });
const { data: basicWeather } = useWeatherAPI({ endpoint: 'baseline' });
const [current, setCurrent] = useState<CurrentWeather>({
icon: '',
temperature: 0,
highTemp: 0,
lowTemp: 0,
precipitation: 'N/A',
humidity: 0,
heatIndex: 0,
windSpeed: 0,
windDirection: 0,
});
function fixDigits(k: any) {
if (k < 10) {
return `0${k}`;
@@ -107,7 +110,12 @@ function Header({ greet, date, isMobile }: any) {
}}
>
{!isMobile && (
<Group gap={0} align='end' style={{ display: 'flex' }}>
<Group
gap={0}
align='end'
style={{ display: 'flex' }}
onClick={openWeatherModal}
>
<Text
component='h2'
mr={4}
@@ -137,6 +145,16 @@ function Header({ greet, date, isMobile }: any) {
</Text>
</Group>
</Stack>
<WeatherModal
current={current}
outlook={outlook}
hourly={hourly}
closeWeatherModal={closeWeatherModal}
weatherModalOpened={weatherModalOpened}
setValue={setValue}
setProviderLabel={setProviderLabel}
setProviderURL={setProviderURL}
/>
</Stack>
);
}

View File

@@ -14,7 +14,7 @@ export default function HelpModal({
const [theme, setTheme] = useAtom(appTheme);
return (
<Modal.Root
size={'50%'}
size={'60%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={providerModalOpened}

View File

@@ -0,0 +1,100 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { Box, Card, Center, Image, Stack, Text } from '@mantine/core';
export enum WindDirection {
N = 0,
NE = 45,
E = 90,
SE = 135,
S = 180,
SW = 225,
W = 270,
NW = 315,
}
export default function HourlyCard({ weather }: any) {
return (
<Stack gap={0} align='center'>
<Card
style={{
height: '8rem',
width: '4rem',
backgroundColor: 'var(--color-text-pri)',
background: 'rgba(255, 255, 255, 0.6)',
padding: 0,
margin: 0,
}}
>
<Stack gap={0} h={'100%'} w={'100%'} align='center'>
<Stack gap={0} align='center'>
<Text mb={-6} fz={12} style={{ zIndex: 2 }}>
{new Date(weather.startTime).toLocaleString('en-US', {
weekday: 'short',
month: 'numeric',
day: 'numeric',
})}
</Text>
<Text fz={12} style={{ zIndex: 2 }}>
{new Date(weather.startTime).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: 'numeric',
})}
</Text>
<Stack gap={0} align='center'>
<Box
style={{
position: 'absolute',
width: '56px',
height: '56px',
backgroundColor: 'transparent',
boxShadow:
'inset 0px 0px 10px 3px rgba(255, 255, 255, 1), 0px 0px 10px 0px rgba(255, 255, 255, .6)',
zIndex: 1,
borderRadius: '100%',
borderStyle: 'none',
border: '0px solid',
}}
/>
<Image
src={weather.icon}
style={{
zIndex: 0,
borderRadius: '100%',
border: '0px solid',
borderStyle: 'none',
}}
/>
</Stack>
</Stack>
<Stack gap={0} align='center' style={{ zIndex: 1 }}>
<Center>
<Icon
icon={'mdi:navigation'}
style={{
position: 'absolute',
transform: `rotate(${WindDirection[weather.windDirection]}deg)`,
}}
/>
<Icon fontSize={28} icon={'mdi:circle-outline'} />
</Center>
<Text mt={-6} fz={12}>
{weather.windSpeed}
</Text>
</Stack>
</Stack>
</Card>
<Text
mt={4}
w={'100%'}
fz={24}
style={{
textAlign: 'center',
borderRadius: '4px',
background: 'rgba(0, 0, 0, 0.6)',
}}
>
{weather.temperature}°{weather.temperatureUnit}
</Text>
</Stack>
);
}

View File

@@ -1,42 +1,23 @@
import { Stack, TextInput } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useQueryAPI, useWeatherAPI } from '../../../hooks';
import { useState } from 'react';
import { useQueryAPI } from '../../../hooks';
import { textInputClasses } from '../../../styles';
import HelpModal from './HelpModal';
import WeatherModal from './WeatherModal';
export default function Search() {
export default function Search({
current,
outlook,
hourly,
weatherModalOpened,
closeWeatherModal,
openWeatherModal,
}: any) {
const { data: providers } = useQueryAPI({ endpoint: 'api/providers' });
const { data: basicWeather } = useWeatherAPI({ endpoint: 'baseline' });
const { data: weather } = useWeatherAPI({ endpoint: 'current' });
const [current, setCurrent] = useState<CurrentWeather>({
icon: '',
temperature: 0,
highTemp: 0,
lowTemp: 0,
precipitation: 'N/A',
humidity: 0,
heatIndex: 0,
windSpeed: 0,
windDirection: 0,
});
const [outlook, setOutlook] = useState<OutlookWeather[]>([
{
name: '',
icon: '',
isDayTime: true,
temperature: 0,
precipitation: 0,
windSpeed: '0 mph',
windDirection: 'N',
},
]);
const { data: forecast } = useWeatherAPI({ endpoint: 'forecast' });
const [providerModalOpened, { open: openProviderModal, close: closeProviderModal }] =
useDisclosure(false);
const [weatherModalOpened, { open: openWeatherModal, close: closeWeatherModal }] =
useDisclosure(false);
const [providerLabel, setProviderLabel] = useState('Google Search');
const [providerURL, setProviderURL] = useState('https://www.google.com/search?q=');
const [value, setValue] = useState('');
@@ -88,59 +69,6 @@ export default function Search() {
return !!pattern.test(str);
}
useEffect(() => {
if (weather && basicWeather) {
setCurrent({
icon: weather.features[0].properties.icon?.replace('medium', 'large') || '',
temperature: Math.round(
(weather.features[0].properties.temperature.value * 9) / 5 + 32,
),
highTemp: Math.round(
(basicWeather.properties.maxTemperature.values[0].value * 9) / 5 + 32,
),
lowTemp: Math.round(
(basicWeather.properties.minTemperature.values[0].value * 9) / 5 + 32,
),
precipitation:
weather.features[0].properties.precipitationLastHour.value || 'N/A',
humidity: Math.round(weather.features[0].properties.relativeHumidity.value),
heatIndex: Math.round(
(weather.features[0].properties.heatIndex.value * 9) / 5 + 32,
),
windSpeed: Math.round(weather.features[0].properties.windSpeed.value / 1.60934),
windDirection: weather.features[0].properties.windDirection.value,
});
}
}, [weather, basicWeather]);
useEffect(() => {
if (forecast) {
setOutlook([
{
name: forecast.properties.periods[0].name,
icon: forecast.properties.periods[0].icon.replace('medium', 'large'),
isDayTime: forecast.properties.periods[0].isDaytime,
temperature: forecast.properties.periods[0].temperature,
precipitation: Math.round(
forecast.properties.periods[0].probabilityOfPrecipitation.value,
),
windSpeed: forecast.properties.periods[0].windSpeed,
windDirection: forecast.properties.periods[0].windDirection,
},
{
name: forecast.properties.periods[1].name,
icon: forecast.properties.periods[1].icon.replace('medium', 'large'),
isDayTime: forecast.properties.periods[1].isDaytime,
temperature: forecast.properties.periods[1].temperature,
precipitation: Math.round(
forecast.properties.periods[1].probabilityOfPrecipitation.value,
),
windSpeed: forecast.properties.periods[1].windSpeed,
windDirection: forecast.properties.periods[1].windDirection,
},
]);
}
}, [forecast]);
return (
<Stack
style={{
@@ -201,6 +129,7 @@ export default function Search() {
<WeatherModal
current={current}
outlook={outlook}
hourly={hourly}
closeWeatherModal={closeWeatherModal}
weatherModalOpened={weatherModalOpened}
setValue={setValue}

View File

@@ -1,7 +1,7 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { Box, Center, Image, Overlay, Stack, Text } from '@mantine/core';
enum WindDirection {
export enum WindDirection {
N = 0,
NE = 45,
E = 90,

View File

@@ -4,11 +4,14 @@ import { modalClasses } from '../../../styles';
import SmallOutlookCard from './SmallOutlookCard';
import SmallWeatherCard from './SmallWeatherCard';
import { useEffect, useState } from 'react';
import { appTheme } from '../../../contexts/theme';
import HourlyCard from './HourlyCard';
export default function WeatherModal({
current,
outlook,
hourly,
closeWeatherModal,
weatherModalOpened,
setValue,
@@ -16,9 +19,25 @@ export default function WeatherModal({
setProviderURL,
}: any) {
const [theme, setTheme] = useAtom(appTheme);
const [hourlyForecast, setHourlyForecast] = useState<any>([]);
const [sunset, setSunset] = useState(100);
useEffect(() => {
if (hourly) {
setHourlyForecast(hourly.properties.periods.slice(1, 13));
}
}, [hourly]);
useEffect(() => {
if (hourlyForecast) {
setSunset(
(hourlyForecast.findIndex((item: any) => item.isDaytime === false) / 12) * 100,
);
}
}, [hourlyForecast]);
return (
<Modal.Root
size={'50%'}
size={'60%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={weatherModalOpened}
@@ -47,13 +66,15 @@ export default function WeatherModal({
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Center w={'100%'}>
<Stack>
<Group gap={0} w={'100%'} align='center' justify='center'>
<Stack gap={0} align='center'>
<h3 style={{ padding: 0, margin: 0 }}>Currently</h3>
<Stack>
<SmallWeatherCard weather={current} />
</Stack>
</Stack>
<Divider orientation='vertical' mx={8} color={theme.accentColor} />
<Divider orientation='vertical' mx={16} color={theme.accentColor} />
<Group gap={8}>
{outlook.map((o: any) => (
<Stack key={o.name} gap={0} align='center'>
@@ -64,6 +85,24 @@ export default function WeatherModal({
</Stack>
))}
</Group>
</Group>
<Divider orientation='horizontal' mx={8} color={theme.accentColor} />
<Group
style={{
background:
hourlyForecast.length > 0 && hourlyForecast[0].isDaytime
? `linear-gradient(90deg, rgba(245, 225, 171, 0.85) ${sunset - 5}%, rgba(35, 28, 42, 0.85) ${sunset + 5}%)`
: `linear-gradient(90deg, rgba(35, 28, 42, 0.85)${sunset - 5}%, rgba(245, 225, 171, 0.85)${sunset + 5}%)`,
padding: '.5rem',
borderRadius: '.5rem',
}}
>
{hourlyForecast.length > 0 &&
hourlyForecast.map((h: any) => {
return <HourlyCard key={h.time} weather={h} />;
})}
</Group>
</Stack>
</Center>
</Modal.Body>
</Modal.Content>

View File

@@ -1,4 +1,4 @@
interface CurrentWeather {
export interface CurrentWeather {
icon: string;
temperature: number;
highTemp: number;
@@ -10,7 +10,7 @@ interface CurrentWeather {
windDirection: number;
}
interface OutlookWeather {
export interface OutlookWeather {
name: string;
icon: string;
isDayTime: boolean;

View File

@@ -28,5 +28,5 @@ export default defineConfig(({mode}) => {
},
},
},
}
};
});