From c61faca9bc87224a1e93bf7ef31ae2dfe04925bd Mon Sep 17 00:00:00 2001 From: Ahmed Elsakaan Date: Fri, 3 Nov 2023 18:29:56 +0000 Subject: [PATCH 1/2] feat: weather, notebooks table and node 20 (#343) This commit brings the following changes: - introduces the side box with weather data - introduces the notebooks table in home dashboard layout - upgrades nodejs to v20 - upgrades pnpm to 8.10.0 --- package.json | 5 +- pnpm-lock.yaml | 3 + .../_components/recent-modules.tsx | 31 +++++----- .../app/_components/notebook-table.tsx | 57 +++++++++++++++++++ .../(dashboard)/app/_components/weather.tsx | 49 ++++++++++++++++ src/app/(dashboard)/app/layout.tsx | 2 +- src/app/(dashboard)/app/page.tsx | 30 ++++++++-- src/env.mjs | 2 + src/hooks/useCoords.ts | 18 ++++++ src/server/api/root.ts | 2 + src/server/api/routers/weather.ts | 43 ++++++++++++++ src/utils/formatDate.ts | 42 ++++++++++++++ src/utils/getFormattedWeatherDescription.ts | 48 ++++++++++++++++ 13 files changed, 306 insertions(+), 26 deletions(-) create mode 100644 src/app/(dashboard)/app/_components/notebook-table.tsx create mode 100644 src/app/(dashboard)/app/_components/weather.tsx create mode 100644 src/hooks/useCoords.ts create mode 100644 src/server/api/routers/weather.ts create mode 100644 src/utils/formatDate.ts create mode 100644 src/utils/getFormattedWeatherDescription.ts diff --git a/package.json b/package.json index 93c52c95..2cbc1ef2 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "lucide-react": "^0.288.0", "next": "13.5.6", "next-themes": "^0.2.1", + "node-fetch": "^3.3.2", "postcss": "8.4.31", "react": "18.2.0", "react-animate-height": "^3.2.2", @@ -112,7 +113,7 @@ "tsx": "^3.14.0" }, "volta": { - "node": "18.18.2", - "pnpm": "8.9.2" + "node": "20.9.0", + "pnpm": "8.10.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd0caa8c..d8fca9dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,6 +176,9 @@ dependencies: next-themes: specifier: ^0.2.1 version: 0.2.1(next@13.5.6)(react-dom@18.2.0)(react@18.2.0) + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 postcss: specifier: 8.4.31 version: 8.4.31 diff --git a/src/app/(dashboard)/_components/recent-modules.tsx b/src/app/(dashboard)/_components/recent-modules.tsx index bd129711..fb6165d1 100644 --- a/src/app/(dashboard)/_components/recent-modules.tsx +++ b/src/app/(dashboard)/_components/recent-modules.tsx @@ -1,11 +1,10 @@ "use client"; import { type IconNames } from "@/components/icon"; -import { ScrollArea } from "@/components/scroll-area"; import { trpc } from "@/trpc/client"; import { cn } from "@/utils/cn"; import { Button } from "@nextui-org/react"; -import { type FC, Suspense, useState } from "react"; +import { Suspense, useState, type FC } from "react"; import AnimateHeight from "react-animate-height"; import { ErrorBoundary } from "react-error-boundary"; import { ModuleCard, ModuleCardSkeleton } from "./module-card"; @@ -69,21 +68,19 @@ export const RecentModules = () => { - - Failed to load modules}> - - {new Array(8).fill(0).map((_, i) => ( - - ))} - - } - > - - - - + Failed to load modules}> + + {new Array(8).fill(0).map((_, i) => ( + + ))} + + } + > + + + ); diff --git a/src/app/(dashboard)/app/_components/notebook-table.tsx b/src/app/(dashboard)/app/_components/notebook-table.tsx new file mode 100644 index 00000000..51b27e2f --- /dev/null +++ b/src/app/(dashboard)/app/_components/notebook-table.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { Icon } from "@/components/icon"; +import { + Chip, + Table, + TableBody, + TableCell, + TableColumn, + TableHeader, + TableRow, +} from "@nextui-org/react"; + +const fakeData = { + name: "Introduction to Information Security", + module: "CS759", + lastEdited: "2 Hours ago", +}; + +export const NotebookTable = () => { + return ( + + + + Name + + + Module + + + Last edited + + + + {Array.from({ length: 20 }).map((_, i) => ( + + + {fakeData.name} + + + + {fakeData.module} + + + {fakeData.lastEdited} + + ))} + +
+ ); +}; diff --git a/src/app/(dashboard)/app/_components/weather.tsx b/src/app/(dashboard)/app/_components/weather.tsx new file mode 100644 index 00000000..a0a1a280 --- /dev/null +++ b/src/app/(dashboard)/app/_components/weather.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { type FC } from "react"; + +import { useCoords } from "@/hooks/useCoords"; +import { trpc } from "@/trpc/client"; +import { getFormattedWeatherDescription } from "@/utils/getFormattedWeatherDescription"; +import { Skeleton } from "@nextui-org/react"; + +export const WeatherData: FC = () => { + const coords = useCoords(); + const { data: weatherData, isLoading } = trpc.weather.getWeatherData.useQuery( + { + latitude: coords?.latitude ?? 0, + longitude: coords?.longitude ?? 0, + }, + { + enabled: !!coords, + retry: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }, + ); + + if (isLoading) { + return ( +
+ +
+ + +
+ +
+ ); + } + + if (!weatherData) return null; + + return ( + + You can expect a πŸ‘† high of {weatherData.main.temp_max.toFixed()}ΒΊ and a + πŸ‘‡ low of {weatherData.main.temp_min.toFixed()}ΒΊ with{" "} + {getFormattedWeatherDescription(weatherData.weather[0]?.description)}{" "} + today. + + ); +}; diff --git a/src/app/(dashboard)/app/layout.tsx b/src/app/(dashboard)/app/layout.tsx index 46c025a4..20679ded 100644 --- a/src/app/(dashboard)/app/layout.tsx +++ b/src/app/(dashboard)/app/layout.tsx @@ -9,7 +9,7 @@ export default function AppLayout({ children }: PropsWithChildren) { return ( -
+
{children} diff --git a/src/app/(dashboard)/app/page.tsx b/src/app/(dashboard)/app/page.tsx index 9efbf8d0..67ef2c0c 100644 --- a/src/app/(dashboard)/app/page.tsx +++ b/src/app/(dashboard)/app/page.tsx @@ -1,8 +1,11 @@ import { TypographyH2 } from "@/components/TypographyH2"; import { TypographyP } from "@/components/TypographyP"; +import { formatDate } from "@/utils/formatDate"; import { currentUser } from "@clerk/nextjs"; import { type Metadata } from "next"; import { RecentModules } from "../_components/recent-modules"; +import { NotebookTable } from "./_components/notebook-table"; +import { WeatherData } from "./_components/weather"; export const metadata: Metadata = { title: "Dashboard Home", @@ -45,13 +48,28 @@ export default async function DashboardHome() { const message = `"${quote.content}" - ${quote.author}`; return ( -
- {greet} - - {message} - +
+
+ {greet} + + {message} + - + + +

Notebooks

+
+
+ +
+
+
+ +
+ It's +

{formatDate(new Date())}

+ +
); } diff --git a/src/env.mjs b/src/env.mjs index 8d3a2ed9..5639d86f 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -12,6 +12,7 @@ export const env = createEnv({ UPSTASH_REDIS_REST_URL: z.string().min(1), UPSTASH_REDIS_REST_TOKEN: z.string().min(1), CLERK_SECRET_KEY: z.string().min(1), + OPENWEATHER_API_KEY: z.string().min(1), }, client: { NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), @@ -25,6 +26,7 @@ export const env = createEnv({ process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, + OPENWEATHER_API_KEY: process.env.OPENWEATHER_API_KEY, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/src/hooks/useCoords.ts b/src/hooks/useCoords.ts new file mode 100644 index 00000000..25e2f3cd --- /dev/null +++ b/src/hooks/useCoords.ts @@ -0,0 +1,18 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +export const useCoords = () => { + const [coords, setCoords] = useState<{ + latitude: number; + longitude: number; + } | null>(null); + + useEffect(() => { + navigator.geolocation.getCurrentPosition((position) => { + setCoords(position.coords); + }); + }, []); + + return coords; +}; \ No newline at end of file diff --git a/src/server/api/root.ts b/src/server/api/root.ts index e0649c9f..dbc936a2 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,9 +1,11 @@ import { feedbackRouter } from "./routers/feedback"; import { moduleRouter } from "./routers/module"; import { waitlistRouter } from "./routers/waitlist"; +import { weatherRouter } from "./routers/weather"; import { createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ + weather: weatherRouter, waitlist: waitlistRouter, feedback: feedbackRouter, module: moduleRouter, diff --git a/src/server/api/routers/weather.ts b/src/server/api/routers/weather.ts new file mode 100644 index 00000000..4580836c --- /dev/null +++ b/src/server/api/routers/weather.ts @@ -0,0 +1,43 @@ +import { TRPCError } from "@trpc/server"; +import fetch from "node-fetch"; +import { z } from "zod"; + +import { env } from "@/env.mjs"; + +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +type WeatherData = { + main: { + temp_max: number; + temp_min: number; + }; + weather: { + description: string; + }[]; +}; + +export const weatherRouter = createTRPCRouter({ + getWeatherData: protectedProcedure + .input( + z.object({ + latitude: z.number(), + longitude: z.number(), + }), + ) + .query(async ({ input }) => { + const { latitude, longitude } = input; + + const response = await fetch( + `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${env.OPENWEATHER_API_KEY}&units=metric`, + ); + + if (!response.ok) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch weather data", + }); + } + + return response.json() as Promise; + }), +}); diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 00000000..692aa225 --- /dev/null +++ b/src/utils/formatDate.ts @@ -0,0 +1,42 @@ +export function formatDate(date: Date) { + const daysOfWeek = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + + const day = date.getDate(); + let daySuffix = "th"; + + if (day === 1 || day === 21 || day === 31) { + daySuffix = "st"; + } else if (day === 2 || day === 22) { + daySuffix = "nd"; + } else if (day === 3 || day === 23) { + daySuffix = "rd"; + } + + const dayOfWeek = daysOfWeek[date.getDay()]; + const month = months[date.getMonth()]; + const year = date.getFullYear(); + + return `${dayOfWeek}, ${day}${daySuffix} of ${month} ${year}`; +} diff --git a/src/utils/getFormattedWeatherDescription.ts b/src/utils/getFormattedWeatherDescription.ts new file mode 100644 index 00000000..2fa4cdc7 --- /dev/null +++ b/src/utils/getFormattedWeatherDescription.ts @@ -0,0 +1,48 @@ +export const getFormattedWeatherDescription = ( + condition: string | undefined, +) => { + if (!condition) return; + + const weatherToEmoji: Record = { + "clear sky": "β˜€οΈ", + "few clouds": "🌀️", + "scattered clouds": "β›…", + "broken clouds": "☁️", + "overcast clouds": "☁️", + rain: "🌧️", + "light rain": "🌧️", + "moderate rain": "🌧️", + "heavy intensity rain": "🌧️", + "very heavy rain": "🌧️", + "extreme rain": "🌧️", + "freezing rain": "🌨️", + "light intensity shower rain": "🌦️", + "shower rain": "🌧️", + "heavy intensity shower rain": "🌧️", + "ragged shower rain": "🌧️", + "light snow": "❄️", + snow: "❄️", + "heavy snow": "❄️", + sleet: "🌨️", + "shower sleet": "🌨️", + "light rain and snow": "🌨️", + "rain and snow": "🌨️", + "light shower snow": "🌨️", + "shower snow": "🌨️", + "heavy shower snow": "🌨️", + mist: "🌫️", + smoke: "🌫️", + haze: "🌫️", + "sand/ dust whirls": "πŸŒͺ️", + fog: "🌫️", + sand: "🌫️", + dust: "🌫️", + "volcanic ash": "🌫️", + squalls: "🌬️", + tornado: "πŸŒͺ️", + clear: "β˜€οΈ", + clouds: "☁️", + }; + + return `${weatherToEmoji[condition.toLowerCase()] ?? ""} ${condition}`; +}; From a04a93b14e2aea62f377816eb78e1cf81bc23ebf Mon Sep 17 00:00:00 2001 From: Ahmed Elsakaan Date: Fri, 3 Nov 2023 19:45:30 +0000 Subject: [PATCH 2/2] fix formatting --- src/hooks/useCoords.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useCoords.ts b/src/hooks/useCoords.ts index 25e2f3cd..9cdcb05b 100644 --- a/src/hooks/useCoords.ts +++ b/src/hooks/useCoords.ts @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; export const useCoords = () => { const [coords, setCoords] = useState<{ @@ -15,4 +15,4 @@ export const useCoords = () => { }, []); return coords; -}; \ No newline at end of file +};