diff --git a/app/globals.css b/app/globals.css index 9c42d5f..c8c52d4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -76,4 +76,6 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +} + +@import url(./index.css); \ No newline at end of file diff --git a/app/index.css b/app/index.css new file mode 100644 index 0000000..7e8fdc5 --- /dev/null +++ b/app/index.css @@ -0,0 +1 @@ +@import url(../fonts/weather-icons.min.css); \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 0fd92b0..f40799b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -30,7 +30,9 @@ export default function RootLayout({ -
{children}
+
+ {children} +
diff --git a/app/page.tsx b/app/page.tsx index aeed5de..080c020 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,9 @@ +import City from '@/components/screen/City' +import Current from '@/components/screen/Current' +import CurrentDetail from '@/components/screen/CurrentDetail' import NavBar from '@/components/screen/NavBar' +import { getLocation, getWeather } from '@/lib/api' +import { Suspense } from 'react' export type Params = { lat?: `${number}` @@ -13,12 +18,36 @@ type PageProps = { searchParams?: Params } -export default function Page(props: PageProps) { +export default async function Page(props: PageProps) { const { lat, lon, city, country, units } = props.searchParams as Params + const weatherPromise = getWeather(lat, lon) + const locationPromise = getLocation(lat, lon, city, country) + const weather = await weatherPromise + const timezone = + weather?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone + return ( <> - + + {weather?.error ? ( +
+ Error: {weather?.error || 'Something went wrong'} +
+ ) : ( + <> + Loading...}> + + + + +
{JSON.stringify(weather, null, 2)}
+ + )} ) } diff --git a/components/atom/toggle-units.tsx b/components/atom/toggle-units.tsx new file mode 100644 index 0000000..6893010 --- /dev/null +++ b/components/atom/toggle-units.tsx @@ -0,0 +1,52 @@ +'use client' + +import { useRouter, useSearchParams } from 'next/navigation' +import { Button } from '../ui/button' +import { Params } from '@/app/page' + +type Queries = { + key: string + value: string +} + +export default function ToggleUnits({ units }: { units: Params['units'] }) { + const router = useRouter() + const query = useSearchParams() + const params = query?.entries() + const queries: Queries[] = [] + + if (!params) + for (const [key, value] of params as any) { + queries.push({ key, value }) + } + + const hasUnits = queries.some((query) => query.key === 'units') + + return ( + + ) +} diff --git a/components/atom/use-gps.tsx b/components/atom/use-gps.tsx new file mode 100644 index 0000000..fe2f35c --- /dev/null +++ b/components/atom/use-gps.tsx @@ -0,0 +1,56 @@ +'use client' + +import React, { useCallback, useEffect, useRef } from 'react' +import { Button } from '../ui/button' +import { Icons } from '../ui/icons' +import { useRouter } from 'next/navigation' +import { LucideIcon } from 'lucide-react' + +export default function GetGPS() { + const router = useRouter() + const icon = useRef(Icons.gps) // using a ref to change the icon so that the data comes in html from server + + const success = useCallback( + async (position: GeolocationPosition) => { + const { latitude, longitude } = position.coords + + localStorage.setItem('gps-granted', String(true)) + icon.current = Icons.gpsFixed + router.replace(`/?lat=${latitude}&lon=${longitude}`) + console.log('success') + }, + [router] + ) + + function onError() { + localStorage.removeItem('gps-granted') + icon.current = Icons.gps + console.log('error') + } + + const getGeoLocation = useCallback(() => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + success, + (_e) => { + onError() + }, + { + enableHighAccuracy: true, + } + ) + } + }, [success]) + + useEffect(() => { + if (localStorage.getItem('gps-granted')) { + getGeoLocation() + } + }, [getGeoLocation]) + + return ( + + ) +} diff --git a/components/screen/City.tsx b/components/screen/City.tsx new file mode 100644 index 0000000..84063a1 --- /dev/null +++ b/components/screen/City.tsx @@ -0,0 +1,18 @@ +import type { location } from '@/utils/types' +import { Separator } from '../ui/separator' + +type CityProps = { + promise: Promise +} + +export default async function CurrentDetails({ promise }: CityProps) { + const [location] = await promise + return ( + <> + +

+ {location?.name}, {location?.country} +

+ + ) +} diff --git a/components/screen/Current.tsx b/components/screen/Current.tsx new file mode 100644 index 0000000..4c326de --- /dev/null +++ b/components/screen/Current.tsx @@ -0,0 +1,74 @@ +import { cn } from '@/lib/utils' +import { getIfDay } from '@/utils' +import { getWindCondition, tempValue } from '@/utils/constants' +import { Separator } from '../ui/separator' +import { Icons } from '../ui/icons' + +type CurrentProps = { + daily: { + temp: { + min: number + max: number + } + }[] + current: { + temp: number + feels_like: number + weather: { + id: number + description: string + main: string + }[] + wind_speed: number + sunrise: number + sunset: number + } + units: boolean +} + +export default function Current({ daily, current, units }: CurrentProps) { + return ( + <> +
+
+

+ {tempValue(current.temp, units)}° +

+

+ + {' '} + {tempValue(daily[0].temp.min, units)} + ° + + + {' '} + {tempValue(daily[0].temp.max, units)} + ° + +

+
+

+ {current.weather[0].main} •{' '} + {getWindCondition(current.wind_speed)?.condition} +

+

+ Feels like {tempValue(current.feels_like, units)}° +

+
+
+
+ +

{current.weather[0].description}

+
+
+ + + ) +} diff --git a/components/screen/CurrentDetail.tsx b/components/screen/CurrentDetail.tsx new file mode 100644 index 0000000..a4df117 --- /dev/null +++ b/components/screen/CurrentDetail.tsx @@ -0,0 +1,11 @@ +import { tellMeRain } from '@/utils' + +export default function CurrentDetail({ minutely }) { + return ( + <> +
+

{tellMeRain(minutely)}

+
+ + ) +} diff --git a/components/screen/NavBar.tsx b/components/screen/NavBar.tsx index 318a31e..5deafed 100644 --- a/components/screen/NavBar.tsx +++ b/components/screen/NavBar.tsx @@ -1,31 +1,49 @@ -import { Button } from '@/components/ui/button' import CommandMenu from '../atom/command-menu' -import { Icons } from '../ui/icons' import ModeToggle from '../atom/toggle-menu' import type { Params } from '@/app/page' +import GetGPS from '../atom/use-gps' +import { getTime } from '@/utils' +import ToggleUnits from '../atom/toggle-units' -type NavBarProps = Pick +type NavBarProps = { + units: Params['units'] + timezone: string +} -function NavBar({ units }: NavBarProps) { +function NavBar({ units, timezone }: NavBarProps) { return ( ) diff --git a/components/ui/icons.tsx b/components/ui/icons.tsx index 60e8f03..b49062a 100644 --- a/components/ui/icons.tsx +++ b/components/ui/icons.tsx @@ -4,7 +4,10 @@ import { Locate, Search, Laptop, + LocateFixed, type Icon as LucideIcon, + ChevronUp, + ChevronDown, } from 'lucide-react' export type Icon = LucideIcon @@ -13,6 +16,9 @@ export const Icons = { moon: Moon, sun: SunMedium, gps: Locate, + gpsFixed: LocateFixed, search: Search, laptop: Laptop, + chevup: ChevronUp, + chevdown: ChevronDown, } diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx new file mode 100644 index 0000000..6c55e0b --- /dev/null +++ b/components/ui/separator.tsx @@ -0,0 +1,31 @@ +'use client' + +import * as React from 'react' +import * as SeparatorPrimitive from '@radix-ui/react-separator' + +import { cn } from '@/lib/utils' + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = 'horizontal', decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..d8c488c --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,66 @@ +import { londonGeo } from '@/utils/constants' + +export async function getLocation( + lat: string | undefined, + lon: string | undefined, + name: string | undefined, + country: string | undefined +): Promise<{ name: string; country: string }[]> { + if (lat && lon) { + const reverseGeoURL = `${process.env.REVERSE_GEO_URL}&lat=${lat}&lon=${lon}` + try { + const res = await fetch(reverseGeoURL, { + cache: 'force-cache', + }) + + if (!res.ok) { + const mes = await res.json() + throw new Error(mes.message) + } + + return res.json() + } catch (e: any) { + console.log(e) + return [{ name: 'London', country: 'GB' }] + } + } + if (name && country) { + return [ + { + name, + country, + }, + ] + } + return [ + { + name: 'London', + country: 'GB', + }, + ] +} + +export async function getWeather( + lat: string | undefined, + lon: string | undefined +) { + const latitude = lat || londonGeo.lat + const longitude = lon || londonGeo.lon + const weatherDataURL = `${process.env.ONECALL_URL}&lat=${latitude}&lon=${longitude}` + try { + const res = await fetch(weatherDataURL, { + next: { + revalidate: 60 * 15, + }, + }) + + if (!res.ok) { + const mes = await res.json() + throw new Error(mes.message) + } + + return res.json() + } catch (e: any) { + return { error: e?.message || 'Something went wrong' } + } +} diff --git a/next.config.js b/next.config.js index 767719f..a843cbe 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,6 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + reactStrictMode: true, +} module.exports = nextConfig diff --git a/package.json b/package.json index 3891078..1c1c5d9 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.6.0", "cmdk": "^0.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf98e2b..8abf258 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@radix-ui/react-dropdown-menu': specifier: ^2.0.5 version: 2.0.5(@types/react-dom@18.2.6)(@types/react@18.2.13)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.13)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.13)(react@18.2.0) @@ -858,6 +861,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.13)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.5 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.13)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.13 + '@types/react-dom': 18.2.6 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.0(react@18.2.0): resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} peerDependencies: diff --git a/tailwind.config.js b/tailwind.config.js index dc164a1..1494002 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -18,7 +18,7 @@ module.exports = { }, }, extend: { - fontFamily: ['var(--font-montserrat)', ...fontFamily.sans], + fontFamily: { sans: ['var(--font-montserrat)', ...fontFamily.sans] }, colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", diff --git a/utils/constants.ts b/utils/constants.ts index c15c3e8..959dec3 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -3,11 +3,6 @@ export const londonGeo = { lon: 0.1278, } -export const londonCity = { - city: 'London', - fullcity: 'London, Greater London, England, United Kingdom', -} - export const onError = () => { localStorage.removeItem('gps-granted') } @@ -133,3 +128,10 @@ export function getWindCondition(speed: number) { } export const getWindDirectionDeg = (deg: number) => deg + 180 + +export const tempValue = (temp: number, isImeprial = false) => { + if (isImeprial) { + return Math.round(temp * 1.8 + 32) + } + return Math.round(temp) +} diff --git a/utils/types.d.ts b/utils/types.d.ts index 2bd9b63..e3e320b 100644 --- a/utils/types.d.ts +++ b/utils/types.d.ts @@ -14,14 +14,9 @@ export type city = { timezone: string } -type location = { - center: [number, number] - place_name: string - text: string -} - -export type locationType = { - features: location[] +export type location = { + name: string + country: string } export interface searchProps {