diff --git a/package.json b/package.json
index a5729b41..9fc61975 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,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",
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 = () => {
         </Button>
       </div>
       <AnimateHeight height={isExpanded ? "auto" : 0}>
-        <ScrollArea>
-          <ErrorBoundary fallback={<div>Failed to load modules</div>}>
-            <Suspense
-              fallback={
-                <ul className="flex gap-4 overflow-x-auto pb-4 pt-2">
-                  {new Array(8).fill(0).map((_, i) => (
-                    <ModuleCardSkeleton key={i} isLoaded={false} />
-                  ))}
-                </ul>
-              }
-            >
-              <RecentModulesInner />
-            </Suspense>
-          </ErrorBoundary>
-        </ScrollArea>
+        <ErrorBoundary fallback={<div>Failed to load modules</div>}>
+          <Suspense
+            fallback={
+              <ul className="flex gap-4 overflow-x-auto pb-4 pt-2">
+                {new Array(8).fill(0).map((_, i) => (
+                  <ModuleCardSkeleton key={i} isLoaded={false} />
+                ))}
+              </ul>
+            }
+          >
+            <RecentModulesInner />
+          </Suspense>
+        </ErrorBoundary>
       </AnimateHeight>
     </div>
   );
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 (
+    <Table
+      isHeaderSticky
+      isVirtualized
+      fullWidth
+      removeWrapper
+      aria-label="Example static collection table"
+    >
+      <TableHeader className="border-b border-default-200">
+        <TableColumn className="border-b border-default-200 bg-background dark:border-default-50">
+          Name
+        </TableColumn>
+        <TableColumn className="border-b border-default-200 bg-background dark:border-default-50">
+          Module
+        </TableColumn>
+        <TableColumn className="border-b border-default-200 bg-background dark:border-default-50">
+          Last edited
+        </TableColumn>
+      </TableHeader>
+      <TableBody>
+        {Array.from({ length: 20 }).map((_, i) => (
+          <TableRow key={i}>
+            <TableCell className="flex items-center gap-3">
+              <Icon name="Lock" size={13} strokeWidth={2} /> {fakeData.name}
+            </TableCell>
+            <TableCell>
+              <Chip color="success" size="sm">
+                {fakeData.module}
+              </Chip>
+            </TableCell>
+            <TableCell>{fakeData.lastEdited}</TableCell>
+          </TableRow>
+        ))}
+      </TableBody>
+    </Table>
+  );
+};
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 (
+      <div>
+        <Skeleton className="mt-2 w-[258px] rounded-md">
+          <div className="h-4" />
+        </Skeleton>
+        <Skeleton className="mt-2 w-3/4 rounded-md">
+          <div className="h-4" />
+        </Skeleton>
+      </div>
+    );
+  }
+
+  if (!weatherData) return null;
+
+  return (
+    <span className="text-tiny text-default-500">
+      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.
+    </span>
+  );
+};
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 (
     <TRPCReactProvider headers={headers()}>
       <SideMenuProvider>
-        <div className="relative flex min-h-screen w-screen overflow-hidden">
+        <div className="relative flex h-screen w-screen overflow-hidden">
           <DashboardSideMenu />
 
           <MainDashboardWrapper>{children}</MainDashboardWrapper>
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 (
-    <div>
-      <TypographyH2>{greet}</TypographyH2>
-      <TypographyP className="max-w-prose opacity-75 [&:not(:first-child)]:mt-2">
-        {message}
-      </TypographyP>
+    <div className="grid h-full grid-cols-[1fr_auto] gap-6 overflow-hidden pb-8">
+      <div className="flex flex-col">
+        <TypographyH2>{greet}</TypographyH2>
+        <TypographyP className="max-w-prose opacity-75 [&:not(:first-child)]:mt-2">
+          {message}
+        </TypographyP>
 
-      <RecentModules />
+        <RecentModules />
+
+        <h2 className="mb-2 mt-6 text-large font-semibold">Notebooks</h2>
+        <div className="relative flex h-full flex-col overflow-y-auto">
+          <div className="flex-grow-1 flex-basis-auto absolute h-full w-full flex-shrink-0 overflow-y-scroll rounded-xl border border-default-200 dark:border-default-50">
+            <NotebookTable />
+          </div>
+        </div>
+      </div>
+
+      <div className="mt-8 w-[310px] rounded-2xl border border-default-200 p-6 dark:border-default-50">
+        <span className="text-tiny text-default-500">It&apos;s</span>
+        <h3 className="font-medium text-amber-500">{formatDate(new Date())}</h3>
+        <WeatherData />
+      </div>
     </div>
   );
 }
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..9cdcb05b
--- /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;
+};
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<WeatherData>;
+    }),
+});
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<string, string> = {
+    "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}`;
+};