diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 3216b38..fd90616 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1 @@ -export { useQueryAPI } from './useAPI'; +export { useQueryAPI, useWeatherAPI } from './useAPI'; diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 20e7e51..b7e36f4 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -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, diff --git a/src/pages/homepage/components/Search.tsx b/src/pages/homepage/components/Search.tsx index 6ca5cf0..a902af8 100644 --- a/src/pages/homepage/components/Search.tsx +++ b/src/pages/homepage/components/Search.tsx @@ -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({ + icon: '', + temperature: 0, + highTemp: 0, + lowTemp: 0, + precipitation: 'N/A', + humidity: 0, + heatIndex: 0, + windSpeed: 0, + windDirection: 0, + }); + const [outlook, setOutlook] = useState([ + { + 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 ( { 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() { + { + 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 + > + + + + Weather Forecast + + + +
+ +

Currently

+ + + +
+ + + {outlook.map((o: any) => ( + +

{o.name}

+ + + +
+ ))} +
+
+
+
+
); } diff --git a/src/pages/homepage/components/SmallOutlookCard.tsx b/src/pages/homepage/components/SmallOutlookCard.tsx new file mode 100644 index 0000000..03383f1 --- /dev/null +++ b/src/pages/homepage/components/SmallOutlookCard.tsx @@ -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 ( +
+ + + + + + +
+ + +
+ + {weather.windSpeed} + +
+ + + Precip: + + + {weather.precipitation}% + + + {weather.isDayTime ? 'High of:' : 'Low of:'} + + + {weather.temperature}°F + + +
+
+ ); +} diff --git a/src/pages/homepage/components/SmallWeatherCard.tsx b/src/pages/homepage/components/SmallWeatherCard.tsx new file mode 100644 index 0000000..8186e7e --- /dev/null +++ b/src/pages/homepage/components/SmallWeatherCard.tsx @@ -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 ( +
+ + + + + + + {weather.highTemp}°F/{weather.lowTemp}°F + + + {weather.precipitation !== 'N/A' && ( + + + {weather.precipitation} mm + + )} + {weather.precipitation === 'N/A' && ( + + )} +
+ + +
+ + {weather.windSpeed} mph + +
+ + + {weather.temperature}°F + + + Feels like: {weather.heatIndex} °F + + + Humidity: {weather.humidity}% + + +
+
+ ); +}