Skip to content

Commit

Permalink
feat: ✨ city and current weather
Browse files Browse the repository at this point in the history
  • Loading branch information
AnilSeervi committed Jul 1, 2023
1 parent 7fa23b2 commit 4c46467
Show file tree
Hide file tree
Showing 19 changed files with 421 additions and 31 deletions.
4 changes: 3 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,6 @@
body {
@apply bg-background text-foreground;
}
}
}

@import url(./index.css);
1 change: 1 addition & 0 deletions app/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import url(../fonts/weather-icons.min.css);
4 changes: 3 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export default function RootLayout({
<html lang='en' suppressHydrationWarning>
<body className={cn(montserrat.className, weatherIcons.variable, 'my-4')}>
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
<main className='mx-auto max-w-screen-lg px-6'>{children}</main>
<main className='mx-auto max-w-screen-lg px-6 text-sm'>
{children}
</main>
</ThemeProvider>
</body>
</html>
Expand Down
33 changes: 31 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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}`
Expand All @@ -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 (
<>
<NavBar units={units} />
<NavBar units={units} timezone={timezone} />
{weather?.error ? (
<div className='text-red-500'>
Error: {weather?.error || 'Something went wrong'}
</div>
) : (
<>
<Suspense fallback={<div>Loading...</div>}>
<City promise={locationPromise} />
</Suspense>
<Current
daily={weather.daily}
current={weather.current}
units={units === 'imperial'}
/>
<CurrentDetail minutely={weather.minutely} />
<pre className='text-xs'>{JSON.stringify(weather, null, 2)}</pre>
</>
)}
</>
)
}
52 changes: 52 additions & 0 deletions components/atom/toggle-units.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
variant='ghost'
size='icon'
className='text-base'
onClick={() => {
router.replace(
`/?${
hasUnits
? queries
.map((query) => {
if (query.key === 'units') {
return `units=${
units === 'imperial' ? 'metric' : 'imperial'
}`
}
return `${query.key}=${query.value}`
})
.join('&')
: `units=${units === 'imperial' ? 'metric' : 'imperial'}`
}`
)
}}
>
{units === 'imperial' ? 'F' : 'C'}&deg;
</Button>
)
}
56 changes: 56 additions & 0 deletions components/atom/use-gps.tsx
Original file line number Diff line number Diff line change
@@ -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<LucideIcon>(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 (
<Button size='icon' variant='ghost' onClick={() => getGeoLocation()}>
{React.createElement(icon.current)}
</Button>
)
}
18 changes: 18 additions & 0 deletions components/screen/City.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { location } from '@/utils/types'
import { Separator } from '../ui/separator'

type CityProps = {
promise: Promise<location[]>
}

export default async function CurrentDetails({ promise }: CityProps) {
const [location] = await promise
return (
<>
<Separator className='mb-4 mt-1' />
<h1 className='mb-2 text-2xl lg:text-3xl'>
{location?.name}, {location?.country}
</h1>
</>
)
}
74 changes: 74 additions & 0 deletions components/screen/Current.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<section className='flex items-center justify-between'>
<div>
<h3 className='text-8xl font-medium leading-tight'>
{tempValue(current.temp, units)}&deg;
</h3>
<p className='mb-7 flex items-center gap-1 text-xs'>
<span className='flex items-center'>
<Icons.chevdown className='inline h-5 w-5 p-0.5' />{' '}
{tempValue(daily[0].temp.min, units)}
&deg;
</span>
<span className='flex items-center'>
<Icons.chevup className='inline h-5 w-5 p-0.5' />{' '}
{tempValue(daily[0].temp.max, units)}
&deg;
</span>
</p>
<div>
<p>
{current.weather[0].main} &bull;{' '}
{getWindCondition(current.wind_speed)?.condition}
</p>
<p className='text-xs leading-normal'>
Feels like {tempValue(current.feels_like, units)}&deg;
</p>
</div>
</div>
<div className='flex flex-col items-center'>
<i
className={cn(
`wi wi-owm${
!getIfDay(current.sunrise, current.sunset) ? '-night' : ''
}-${current.weather[0].id}`,
'm-4 text-8xl'
)}
></i>
<p className='text-xs'>{current.weather[0].description}</p>
</div>
</section>
<Separator className='my-4' />
</>
)
}
11 changes: 11 additions & 0 deletions components/screen/CurrentDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { tellMeRain } from '@/utils'

export default function CurrentDetail({ minutely }) {
return (
<>
<section>
<p className='text-center'>{tellMeRain(minutely)}</p>
</section>
</>
)
}
42 changes: 30 additions & 12 deletions components/screen/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -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<Params, 'units'>
type NavBarProps = {
units: Params['units']
timezone: string
}

function NavBar({ units }: NavBarProps) {
function NavBar({ units, timezone }: NavBarProps) {
return (
<nav className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<Button size='icon' variant='ghost'>
<Icons.gps />
</Button>
<GetGPS />
<CommandMenu />
</div>
<div className='flex flex-col items-center'>
<span className='text-sm lg:text-base'>Wed, July 6</span>
<span className='text-sm lg:text-base'>
{getTime(
{
weekday: 'short',
month: 'short',
day: 'numeric',
},
timezone,
Date.now() / 1000
)}
</span>
<span className='text-[10px] text-neutral-500 lg:text-xs'>
12:00 PM EDT
{getTime(
{
hour: 'numeric',
minute: 'numeric',
timeZoneName: 'short',
},
timezone,
Date.now() / 1000
)}
</span>
</div>
<div className='flex items-center gap-3'>
<ModeToggle />
<Button variant='ghost' size='icon' className='text-base'>
{units === 'imperial' ? 'F' : 'C'}&deg;
</Button>
<ToggleUnits units={units} />
</div>
</nav>
)
Expand Down
6 changes: 6 additions & 0 deletions components/ui/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
Locate,
Search,
Laptop,
LocateFixed,
type Icon as LucideIcon,
ChevronUp,
ChevronDown,
} from 'lucide-react'

export type Icon = LucideIcon
Expand All @@ -13,6 +16,9 @@ export const Icons = {
moon: Moon,
sun: SunMedium,
gps: Locate,
gpsFixed: LocateFixed,
search: Search,
laptop: Laptop,
chevup: ChevronUp,
chevdown: ChevronDown,
}
Loading

0 comments on commit 4c46467

Please sign in to comment.