Weather API integration

This commit is contained in:
Sean Edmunds
2024-11-27 16:00:17 -06:00
parent 18585daaec
commit 4490973a28
5 changed files with 330 additions and 10 deletions

View File

@@ -1 +1 @@
export { useQueryAPI } from './useAPI';
export { useQueryAPI, useWeatherAPI } from './useAPI';

View File

@@ -36,6 +36,41 @@ export function useQueryAPI({ endpoint }: { endpoint: string }) {
});
}
export function useWeatherAPI({ endpoint }: { endpoint: string }) {
let url = '';
if (endpoint === 'baseline') {
url = 'https://api.weather.gov/gridpoints/HGX/55,96';
} else if (endpoint === 'current') {
url = 'https://api.weather.gov/stations/KSGR/observations';
} else if (endpoint === 'forecast') {
url = 'https://api.weather.gov/gridpoints/HGX/55,96/forecast';
}
const callEndpoint = async (endpoint: string) => {
const options: object = {
method: 'GET',
headers: {
'Content-Type': 'application/geo+json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Credentials': true,
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-site',
},
};
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return await response.json();
};
return useQuery({
queryKey: ['GET', url],
queryFn: () => callEndpoint(endpoint),
});
}
export function useMutationAPI() {
const callEndpoint = async (
method: string,

View File

@@ -1,14 +1,66 @@
import { Divider, Group, Modal, Stack, Text, TextInput } from '@mantine/core';
import { Center, Divider, Group, Modal, Stack, Text, TextInput } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useAtom } from 'jotai';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { appTheme } from '../../../contexts/theme';
import { useQueryAPI } from '../../../hooks';
import { useQueryAPI, useWeatherAPI } from '../../../hooks';
import { modalClasses, textInputClasses } from '../../../styles';
import SmallWeatherCard from './SmallWeatherCard';
import SmallOutlookCard from './SmallOutlookCard';
interface CurrentWeather{
icon: string;
temperature: number;
highTemp: number;
lowTemp: number;
precipitation: string;
humidity: number;
heatIndex: number;
windSpeed: number;
windDirection: number;
}
interface OutlookWeather{
name: string;
icon: string;
isDayTime: boolean;
temperature: number;
precipitation: number;
windSpeed: string;
windDirection: string;
}
export default function Search() {
const { data: providers } = useQueryAPI({ endpoint: 'api/providers' });
const [opened, { open: openModal, close: closeModal }] = useDisclosure(false);
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('');
@@ -21,14 +73,20 @@ export default function Search() {
item.target.value.substr(1, item.target.value.indexOf(' ') - 1) ||
item.target.value.substr(1);
subtext = item.target.value.substr(2 + option.length);
} else if (item.target.value.startsWith('!')) {
if (item.target.value.startsWith('!w')) {
subtext = item.target.value.substr(2);
}
} else {
subtext = item.target.value;
}
if (item.key === 'Enter') {
if (providerLabel === 'Show help') {
openModal();
openProviderModal();
} else if (item.target.value.startsWith('/')) {
open(`${providerURL}${subtext}`, '_self');
} else if (providerLabel === 'Show weather') {
openWeatherModal();
} else if (validURL(item.target.value)) {
if (containsProtocol(item.target.value)) window.location = item.target.value;
else open(`https://${item.target.value}`, '_self');
@@ -51,10 +109,60 @@ export default function Search() {
return !!pattern.test(str);
}
function containsProtocol(str: any) {
const pattern = /^(https?:\/\/){1}.*/i;
const pattern = /^(https?:\/\/).*/i;
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])
console.log(outlook)
return (
<Stack
style={{
@@ -77,7 +185,11 @@ export default function Search() {
data-active={value !== ''}
onChange={(item: any) => {
setValue(item.currentTarget.value);
if (item.target.value === '/?') {
if (
item.target.value === '/?' ||
item.target.value === '!help' ||
item.target.value === '!h'
) {
setProviderLabel('Show help');
} else if (item.target.value.startsWith('/')) {
const provider = providers?.find(
@@ -85,6 +197,10 @@ export default function Search() {
);
setProviderLabel(provider?.name || 'Invalid Search');
setProviderURL(provider?.url);
} else if (item.target.value.startsWith('!')) {
if (item.target.value === '!w' || item.target.value === '!weather') {
setProviderLabel('Show weather');
}
} else {
setProviderLabel('Google Search');
setProviderURL('https://www.google.com/search?q=');
@@ -100,9 +216,9 @@ export default function Search() {
size={'50%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={opened}
opened={providerModalOpened}
onClose={() => {
closeModal();
closeProviderModal();
setValue('');
setProviderLabel('Google Search');
setProviderURL('https://www.google.com/search?q=');
@@ -138,6 +254,57 @@ export default function Search() {
</Modal.Body>
</Modal.Content>
</Modal.Root>
<Modal.Root
size={'50%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={weatherModalOpened}
onClose={() => {
closeWeatherModal();
setValue('');
setProviderLabel('Google Search');
setProviderURL('https://www.google.com/search?q=');
}}
style={{
'--color-text-pri': theme.primaryColor,
'--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url(${theme.backgroundImage})`,
}}
closeOnEscape
closeOnClickOutside
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Weather Forecast</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Center w={'100%'}>
<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} />
<Group gap={0}>
{outlook.map((o: any) => (
<Stack key={o.name} gap={0} align='center'>
<h3 style={{ padding: 0, margin: 0 }}>{o.name}</h3>
<Stack>
<SmallOutlookCard weather={o} />
</Stack>
</Stack>
))}
</Group>
</Center>
</Modal.Body>
</Modal.Content>
</Modal.Root>
</Stack>
);
}

View File

@@ -0,0 +1,60 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { Box, Center, Image, Overlay, Stack, Text } from '@mantine/core';
enum WindDirection {
N = 0,
NE = 45,
E = 90,
SE = 135,
S = 180,
SW = 225,
W = 270,
NW = 315,
}
export default function SmallOutlookCard({ weather }: any) {
return (
<Center display={'flex'}>
<Box pos={'relative'} style={{ zIndex: -1 }}>
<Image radius={'lg'} src={weather.icon} />
<Overlay
color='black'
gradient='linear-gradient(0deg, rgba(0, 0, 0, .85), rgba(0, 0, 0, 0.35))'
zIndex={0}
radius={'lg'}
/>
</Box>
<Stack h={'124px'} w={'124px'} gap={0} pos={'absolute'}>
<Stack gap={0} align='center' pos={'absolute'} right={0} top={0}>
<Center>
<Icon
icon={'mdi:navigation'}
style={{
position: 'absolute',
transform: `rotate(${WindDirection[weather.windDirection]}deg)`,
}}
/>
<Icon fontSize={32} icon={'mdi:circle-outline'} />
</Center>
<Text mt={-6} fz={12}>
{weather.windSpeed}
</Text>
</Stack>
<Stack gap={0} pos={'absolute'} bottom={8}>
<Text mt={-4} mb={-4} fz={14}>
Precip:
</Text>
<Text mt={-8} mb={-8} p={0} fz={24}>
{weather.precipitation}%
</Text>
<Text mt={-4} mb={-4} fz={14}>
{weather.isDayTime ? 'High of:' : 'Low of:'}
</Text>
<Text mt={-8} mb={-8} p={0} fz={36}>
{weather.temperature}°F
</Text>
</Stack>
</Stack>
</Center>
);
}

View File

@@ -0,0 +1,58 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { Box, Center, Group, Image, Overlay, Stack, Text } from '@mantine/core';
export default function SmallWeatherCard({ weather }: any) {
return (
<Center display={'flex'}>
<Box pos={'relative'} style={{ zIndex: -1 }}>
<Image radius={'lg'} src={weather.icon} />
<Overlay
color='black'
gradient='linear-gradient(0deg, rgba(0, 0, 0, .85), rgba(0, 0, 0, 0.35))'
zIndex={0}
radius={'lg'}
/>
</Box>
<Stack h={'124px'} w={'124px'} gap={0} pos={'absolute'}>
<Text mb={-8} p={0} fz={12}>
{weather.highTemp}°F/{weather.lowTemp}°F
</Text>
<Stack gap={0} align='center' pos={'absolute'} right={0} top={0}>
{weather.precipitation !== 'N/A' && (
<Group gap={0}>
<Icon icon={'mdi:weather-pouring'} height={32} width={32} />
<Text>{weather.precipitation} mm</Text>
</Group>
)}
{weather.precipitation === 'N/A' && (
<Icon icon={'mdi:white-balance-sunny'} height={32} width={32} />
)}
<Center>
<Icon
icon={'mdi:navigation'}
style={{
position: 'absolute',
transform: `rotate(${weather.windDirection}deg)`,
}}
/>
<Icon fontSize={32} icon={'mdi:circle-outline'} />
</Center>
<Text mt={-6} fz={12}>
{weather.windSpeed} mph
</Text>
</Stack>
<Stack gap={0} pos={'absolute'} bottom={8}>
<Text mt={-8} mb={-8} p={0} fz={36}>
{weather.temperature}°F
</Text>
<Text mt={-8} fz={12}>
Feels like: {weather.heatIndex} °F
</Text>
<Text mt={-6} fz={12}>
Humidity: {weather.humidity}%
</Text>
</Stack>
</Stack>
</Center>
);
}