Skip to content

Commit

Permalink
feat: weather, notebooks table and node 20 (#343)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ixahmedxi committed Nov 3, 2023
1 parent 3201d01 commit 0497381
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 26 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 14 additions & 17 deletions src/app/(dashboard)/_components/recent-modules.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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>
);
Expand Down
57 changes: 57 additions & 0 deletions src/app/(dashboard)/app/_components/notebook-table.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
49 changes: 49 additions & 0 deletions src/app/(dashboard)/app/_components/weather.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
2 changes: 1 addition & 1 deletion src/app/(dashboard)/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
30 changes: 24 additions & 6 deletions src/app/(dashboard)/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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>
);
}
2 changes: 2 additions & 0 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
});
18 changes: 18 additions & 0 deletions src/hooks/useCoords.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
43 changes: 43 additions & 0 deletions src/server/api/routers/weather.ts
Original file line number Diff line number Diff line change
@@ -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>;
}),
});
42 changes: 42 additions & 0 deletions src/utils/formatDate.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
Loading

0 comments on commit 0497381

Please sign in to comment.