Skip to content

Commit ae278aa

Browse files
committed
Refactor app structure and add authentication flow
Restructured the app into (auth) and (main) routes, added authentication logic to homepage and profile pages, and implemented an AuthProvider for session management. Added LoginButtons component, updated NavBar with sign out functionality, and created upload notes form. Removed old page and profile files, and added sample PDF uploads for testing.
1 parent ce20e02 commit ae278aa

File tree

12 files changed

+396
-77
lines changed

12 files changed

+396
-77
lines changed

src/app/(auth)/layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function AuthLayout({
2+
children,
3+
}: {
4+
children: React.ReactNode;
5+
}) {
6+
return <>{children}</>;
7+
}

src/app/(auth)/page.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import LoginButtons from '@src/components/LoginButtons';
2+
import { getServerAuthSession } from "@src/server/auth";
3+
import { redirect } from "next/navigation";
4+
5+
6+
7+
const Login = async () => {
8+
const session = await getServerAuthSession();
9+
10+
// If logged in, go straight to homepage
11+
if (session?.user) {
12+
redirect("/homepage");
13+
}
14+
15+
return (
16+
<main className="min-h-screen flex flex-col items-center justify-center gap-6">
17+
<h1 className="text-3xl font-bold text-white">UTD Notebook</h1>
18+
<p className="text-white">Sign in to continue</p>
19+
20+
<LoginButtons />
21+
</main>
22+
);
23+
};
24+
25+
export default Login;

src/app/(main)/homepage/page.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Metadata } from 'next';
2+
import { redirect } from 'next/navigation';
3+
import { getServerAuthSession } from '@src/server/auth';
4+
export const metadata: Metadata = {
5+
alternates: {
6+
canonical: 'https://notebook.utdnebula.com',
7+
},
8+
};
9+
10+
const Home = async () => {
11+
const session = await getServerAuthSession();
12+
13+
// If not logged in, go to login page
14+
if (!session?.user) {
15+
redirect("/");
16+
}
17+
return (
18+
<>
19+
</>
20+
);
21+
};
22+
23+
export default Home;

src/app/(main)/layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// src/app/(main)/layout.tsx
2+
import type { ReactNode } from "react";
3+
import NavBar from '@src/components/NavBar';
4+
5+
export default function MainLayout({ children }: { children: ReactNode }) {
6+
return (
7+
<>
8+
<NavBar />
9+
{children}
10+
</>
11+
);
12+
}

src/app/(main)/profile/page.tsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { getServerAuthSession } from '@src/server/auth';
2+
3+
import Avatar from '@mui/material/Avatar';
4+
import Link from 'next/link';
5+
import UploadNoteForm from '../uploadNotes/uploadNotes';
6+
import { db } from '@src/server/db';
7+
import { eq } from 'drizzle-orm';
8+
9+
export default async function ProfilePage() {
10+
const session = await getServerAuthSession();
11+
12+
if (!session || !session.user) {
13+
return (
14+
<main className="min-h-screen p-8">
15+
<div className="mx-auto max-w-4xl">
16+
<h1 className="text-2xl font-semibold text-white">Unauthorized</h1>
17+
<p className="mt-4 text-white">
18+
You must be logged in to view this page.
19+
</p>
20+
</div>
21+
</main>
22+
);
23+
}
24+
25+
const uploadedFiles = await db.query.file.findMany({
26+
where: (f) => eq(f.authorId, session.user.id),
27+
with: {
28+
section: true,
29+
},
30+
orderBy: (f, { desc }) => [desc(f.createdAt)],
31+
});
32+
33+
const user = {
34+
name: session?.user?.name ?? 'John Doe',
35+
handle: '@johndoe',
36+
email: session?.user?.email ?? '[email protected]',
37+
avatar: session?.user?.image ?? '/images/avatar.jpg',
38+
posts: 0,
39+
reports_submitted: 0,
40+
};
41+
42+
return (
43+
<main className="min-h-screen p-8">
44+
<div className="mx-auto max-w-4xl">
45+
<div className="flex items-center gap-6">
46+
<Avatar
47+
alt={user.name}
48+
src={user.avatar}
49+
sx={{ width: 96, height: 96 }}
50+
/>
51+
<div>
52+
<h1 className="text-2xl font-semibold">{user.name}</h1>
53+
<div className="text-sm text-white">{user.handle}</div>
54+
<p className="mt-2 text-white">{user.email}</p>
55+
<div className="mt-4 flex gap-4 text-sm text-white">
56+
<div>
57+
<strong className="font-bold">{user.posts}</strong> posts
58+
</div>
59+
<div>
60+
<strong className="font-bold">{user.reports_submitted}</strong>{' '}
61+
reports submitted
62+
</div>
63+
</div>
64+
</div>
65+
<div className="ml-auto flex items-center gap-3">
66+
<Link
67+
href="/profile/edit"
68+
className="rounded-md px-4 py-2 text-white"
69+
>
70+
Edit Profile
71+
</Link>
72+
</div>
73+
</div>
74+
<h2 className="mt-10 text-xl font-semibold">Saved Notes:</h2>
75+
<h2 className="mt-10 text-xl font-semibold">Uploaded Notes:</h2>
76+
{uploadedFiles.length === 0 ? (
77+
<p className="mt-2 text-sm text-white/60">
78+
You haven&apos;t uploaded any notes yet.
79+
</p>
80+
) : (
81+
<ul className="mt-4 space-y-2">
82+
{uploadedFiles.map((f) => (
83+
<li
84+
key={f.id}
85+
className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2 text-sm"
86+
>
87+
<div>
88+
<div className="font-medium">{f.fileTitle}</div>
89+
<div className="text-xs text-white/60">
90+
{f.section
91+
? `${f.section.prefix} ${f.section.number}.${f.section.sectionCode}`
92+
: 'N/A'}
93+
• Uploaded {new Date(f.createdAt).toLocaleString()}
94+
</div>
95+
</div>
96+
<Link
97+
href={`/uploads/${f.fileName}`}
98+
target="_blank"
99+
className="text-xs underline"
100+
>
101+
View
102+
</Link>
103+
</li>
104+
))}
105+
</ul>
106+
)}
107+
<UploadNoteForm />
108+
</div>
109+
</main>
110+
);
111+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import {
5+
Box,
6+
Button,
7+
TextField,
8+
MenuItem,
9+
Typography,
10+
Paper,
11+
} from '@mui/material';
12+
import { useRouter } from 'next/navigation';
13+
export default function UploadNoteForm() {
14+
15+
const router = useRouter();
16+
const [file, setFile] = useState<File | null>(null);
17+
const [prefix, setPrefix] = useState('');
18+
const [courseNumber, setCourseNumber] = useState('');
19+
const [sectionCode, setSectionCode] = useState('');
20+
const [professor, setProfessor] = useState('');
21+
const [term, setTerm] = useState('');
22+
const [year, setYear] = useState('');
23+
24+
const termOptions = ['Spring', 'Summer', 'Fall'];
25+
26+
const handleUpload = async () => {
27+
type UploadResponse =
28+
| { error: string }
29+
| { message: string; data?: unknown };
30+
31+
if (!file) {
32+
alert('Please select a file');
33+
return;
34+
}
35+
36+
const formData = new FormData();
37+
formData.append('file', file);
38+
formData.append('prefix', prefix);
39+
formData.append('courseNumber', courseNumber);
40+
formData.append('sectionCode', sectionCode);
41+
formData.append('professor', professor);
42+
formData.append('term', term);
43+
formData.append('year', year);
44+
45+
try {
46+
const res = await fetch('/api/files/upload', {
47+
method: 'POST',
48+
body: formData,
49+
});
50+
51+
const json = (await res.json()) as UploadResponse;
52+
53+
if (!res.ok && 'error' in json) {
54+
alert(json.error || 'Upload failed');
55+
} else {
56+
setFile(null);
57+
setPrefix('');
58+
setCourseNumber('');
59+
setSectionCode('');
60+
setProfessor('');
61+
setTerm('');
62+
setYear('');
63+
alert('Upload successful');
64+
}
65+
} catch (error) {
66+
alert('Upload failed');
67+
console.log(error);
68+
return;
69+
}
70+
router.refresh();
71+
};
72+
73+
return (
74+
<Paper sx={{ p: 3, mt: 3, backgroundColor: '#1E1E1E' }}>
75+
<Typography variant="h6" color="white" gutterBottom>
76+
Upload a New Note
77+
</Typography>
78+
79+
<Box display="flex" flexDirection="column" gap={2}>
80+
<Button variant="contained" component="label">
81+
{file ? file.name : 'Choose File'}
82+
<input
83+
type="file"
84+
hidden
85+
onChange={(e) => setFile(e.target.files?.[0] || null)}
86+
/>
87+
</Button>
88+
89+
<TextField
90+
label="Prefix (e.g. CS)"
91+
value={prefix}
92+
onChange={(e) => setPrefix(e.target.value)}
93+
/>
94+
95+
<TextField
96+
label="Course Number (e.g. 1337)"
97+
value={courseNumber}
98+
onChange={(e) => setCourseNumber(e.target.value)}
99+
/>
100+
101+
<TextField
102+
label="Section Code (e.g. 001)"
103+
value={sectionCode}
104+
onChange={(e) => setSectionCode(e.target.value)}
105+
/>
106+
107+
<TextField
108+
label="Professor"
109+
value={professor}
110+
onChange={(e) => setProfessor(e.target.value)}
111+
/>
112+
113+
<TextField
114+
select
115+
label="Term"
116+
value={term}
117+
onChange={(e) => setTerm(e.target.value)}
118+
>
119+
{termOptions.map((t) => (
120+
<MenuItem key={t} value={t}>
121+
{t}
122+
</MenuItem>
123+
))}
124+
</TextField>
125+
126+
<TextField
127+
label="Year"
128+
type="number"
129+
value={year}
130+
onChange={(e) => setYear(e.target.value)}
131+
/>
132+
133+
<Button
134+
variant="contained"
135+
color="primary"
136+
onClick={() => void handleUpload()}
137+
>
138+
Upload Note
139+
</Button>
140+
</Box>
141+
</Paper>
142+
);
143+
}

src/app/AuthProvider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"use client";
2+
3+
import { SessionProvider } from "next-auth/react";
4+
import type { ReactNode } from "react";
5+
6+
export function AuthProvider({ children }: { children: ReactNode }) {
7+
return <SessionProvider>{children}</SessionProvider>;
8+
}

src/app/layout.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Bai_Jamjuree, Inter } from 'next/font/google';
66
import { type Metadata } from 'next';
77
import { GoogleAnalytics } from '@next/third-parties/google';
88
import Link from 'next/link';
9+
import { AuthProvider } from './AuthProvider';
910

1011
import theme from '@src/utils/theme';
1112
import { ToastProvider } from '@src/components/toast/ToastProvider';
@@ -55,11 +56,15 @@ export default function RootLayout({
5556
<body
5657
className={`bg-white dark:bg-black ${inter.variable} font-main ${baiJamjuree.variable} text-haiti dark:text-white`}
5758
>
58-
<AppRouterCacheProvider>
59-
<ThemeProvider theme={theme}>
60-
<ToastProvider>{children}</ToastProvider>
61-
</ThemeProvider>
62-
</AppRouterCacheProvider>
59+
<AuthProvider>
60+
<AppRouterCacheProvider>
61+
<ThemeProvider theme={theme}>
62+
<ToastProvider>
63+
{children}
64+
</ToastProvider>
65+
</ThemeProvider>
66+
</AppRouterCacheProvider>
67+
</AuthProvider>
6368
{process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && (
6469
<GoogleAnalytics gaId="G-3NDS0P32CZ" />
6570
)}

src/app/page.tsx

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)