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}`;
+};