Weather API integration
This commit is contained in:
@@ -1 +1 @@
|
|||||||
export { useQueryAPI } from './useAPI';
|
export { useQueryAPI, useWeatherAPI } from './useAPI';
|
||||||
|
|||||||
@@ -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() {
|
export function useMutationAPI() {
|
||||||
const callEndpoint = async (
|
const callEndpoint = async (
|
||||||
method: string,
|
method: string,
|
||||||
|
|||||||
@@ -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 { useDisclosure } from '@mantine/hooks';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { appTheme } from '../../../contexts/theme';
|
import { appTheme } from '../../../contexts/theme';
|
||||||
import { useQueryAPI } from '../../../hooks';
|
import { useQueryAPI, useWeatherAPI } from '../../../hooks';
|
||||||
import { modalClasses, textInputClasses } from '../../../styles';
|
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() {
|
export default function Search() {
|
||||||
const { data: providers } = useQueryAPI({ endpoint: 'api/providers' });
|
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 [providerLabel, setProviderLabel] = useState('Google Search');
|
||||||
const [providerURL, setProviderURL] = useState('https://www.google.com/search?q=');
|
const [providerURL, setProviderURL] = useState('https://www.google.com/search?q=');
|
||||||
const [value, setValue] = useState('');
|
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, item.target.value.indexOf(' ') - 1) ||
|
||||||
item.target.value.substr(1);
|
item.target.value.substr(1);
|
||||||
subtext = item.target.value.substr(2 + option.length);
|
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 {
|
} else {
|
||||||
subtext = item.target.value;
|
subtext = item.target.value;
|
||||||
}
|
}
|
||||||
if (item.key === 'Enter') {
|
if (item.key === 'Enter') {
|
||||||
if (providerLabel === 'Show help') {
|
if (providerLabel === 'Show help') {
|
||||||
openModal();
|
openProviderModal();
|
||||||
} else if (item.target.value.startsWith('/')) {
|
} else if (item.target.value.startsWith('/')) {
|
||||||
open(`${providerURL}${subtext}`, '_self');
|
open(`${providerURL}${subtext}`, '_self');
|
||||||
|
} else if (providerLabel === 'Show weather') {
|
||||||
|
openWeatherModal();
|
||||||
} else if (validURL(item.target.value)) {
|
} else if (validURL(item.target.value)) {
|
||||||
if (containsProtocol(item.target.value)) window.location = item.target.value;
|
if (containsProtocol(item.target.value)) window.location = item.target.value;
|
||||||
else open(`https://${item.target.value}`, '_self');
|
else open(`https://${item.target.value}`, '_self');
|
||||||
@@ -51,10 +109,60 @@ export default function Search() {
|
|||||||
return !!pattern.test(str);
|
return !!pattern.test(str);
|
||||||
}
|
}
|
||||||
function containsProtocol(str: any) {
|
function containsProtocol(str: any) {
|
||||||
const pattern = /^(https?:\/\/){1}.*/i;
|
const pattern = /^(https?:\/\/).*/i;
|
||||||
return !!pattern.test(str);
|
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 (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
style={{
|
style={{
|
||||||
@@ -77,7 +185,11 @@ export default function Search() {
|
|||||||
data-active={value !== ''}
|
data-active={value !== ''}
|
||||||
onChange={(item: any) => {
|
onChange={(item: any) => {
|
||||||
setValue(item.currentTarget.value);
|
setValue(item.currentTarget.value);
|
||||||
if (item.target.value === '/?') {
|
if (
|
||||||
|
item.target.value === '/?' ||
|
||||||
|
item.target.value === '!help' ||
|
||||||
|
item.target.value === '!h'
|
||||||
|
) {
|
||||||
setProviderLabel('Show help');
|
setProviderLabel('Show help');
|
||||||
} else if (item.target.value.startsWith('/')) {
|
} else if (item.target.value.startsWith('/')) {
|
||||||
const provider = providers?.find(
|
const provider = providers?.find(
|
||||||
@@ -85,6 +197,10 @@ export default function Search() {
|
|||||||
);
|
);
|
||||||
setProviderLabel(provider?.name || 'Invalid Search');
|
setProviderLabel(provider?.name || 'Invalid Search');
|
||||||
setProviderURL(provider?.url);
|
setProviderURL(provider?.url);
|
||||||
|
} else if (item.target.value.startsWith('!')) {
|
||||||
|
if (item.target.value === '!w' || item.target.value === '!weather') {
|
||||||
|
setProviderLabel('Show weather');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setProviderLabel('Google Search');
|
setProviderLabel('Google Search');
|
||||||
setProviderURL('https://www.google.com/search?q=');
|
setProviderURL('https://www.google.com/search?q=');
|
||||||
@@ -100,9 +216,9 @@ export default function Search() {
|
|||||||
size={'50%'}
|
size={'50%'}
|
||||||
className={modalClasses.helpModal}
|
className={modalClasses.helpModal}
|
||||||
classNames={modalClasses}
|
classNames={modalClasses}
|
||||||
opened={opened}
|
opened={providerModalOpened}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
closeModal();
|
closeProviderModal();
|
||||||
setValue('');
|
setValue('');
|
||||||
setProviderLabel('Google Search');
|
setProviderLabel('Google Search');
|
||||||
setProviderURL('https://www.google.com/search?q=');
|
setProviderURL('https://www.google.com/search?q=');
|
||||||
@@ -138,6 +254,57 @@ export default function Search() {
|
|||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
</Modal.Root>
|
</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>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/pages/homepage/components/SmallOutlookCard.tsx
Normal file
60
src/pages/homepage/components/SmallOutlookCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/pages/homepage/components/SmallWeatherCard.tsx
Normal file
58
src/pages/homepage/components/SmallWeatherCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user