(null);
+ const [prefix, setPrefix] = useState('');
+ const [courseNumber, setCourseNumber] = useState('');
+ const [sectionCode, setSectionCode] = useState('');
+ const [professor, setProfessor] = useState('');
+ const [term, setTerm] = useState('');
+ const [year, setYear] = useState('');
+
+ const termOptions = ['Spring', 'Summer', 'Fall'];
+
+ const handleUpload = async () => {
+ type UploadResponse =
+ | { error: string }
+ | { message: string; data?: unknown };
+
+ if (!file) {
+ alert('Please select a file');
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('prefix', prefix);
+ formData.append('courseNumber', courseNumber);
+ formData.append('sectionCode', sectionCode);
+ formData.append('professor', professor);
+ formData.append('term', term);
+ formData.append('year', year);
+
+ try {
+ const res = await fetch('/api/files/upload', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const json = (await res.json()) as UploadResponse;
+
+ if (!res.ok && 'error' in json) {
+ alert(json.error || 'Upload failed');
+ } else {
+ setFile(null);
+ setPrefix('');
+ setCourseNumber('');
+ setSectionCode('');
+ setProfessor('');
+ setTerm('');
+ setYear('');
+ alert('Upload successful');
+ }
+ } catch (error) {
+ alert('Upload failed');
+ console.log(error);
+ return;
+ }
+ router.refresh();
+ };
+
+ return (
+
+
+ Upload a New Note
+
+
+
+
+
+ setPrefix(e.target.value)}
+ />
+
+ setCourseNumber(e.target.value)}
+ />
+
+ setSectionCode(e.target.value)}
+ />
+
+ setProfessor(e.target.value)}
+ />
+
+ setTerm(e.target.value)}
+ >
+ {termOptions.map((t) => (
+
+ ))}
+
+
+ setYear(e.target.value)}
+ />
+
+
+
+
+ );
+}
diff --git a/src/app/AuthProvider.tsx b/src/app/AuthProvider.tsx
new file mode 100644
index 0000000..7ca2ad5
--- /dev/null
+++ b/src/app/AuthProvider.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import { SessionProvider } from 'next-auth/react';
+import type { ReactNode } from 'react';
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ return {children};
+}
diff --git a/src/app/api/files/upload/route.ts b/src/app/api/files/upload/route.ts
index 86462e0..f3fd2d2 100644
--- a/src/app/api/files/upload/route.ts
+++ b/src/app/api/files/upload/route.ts
@@ -15,10 +15,10 @@ const uploadFormSchema = z.object({
prefix: z.string().min(1, { message: 'Prefix is missing' }),
courseNumber: z.string().min(1, { message: 'Course number is missing' }),
sectionCode: z.string().min(1, { message: 'Section code is missing' }),
- professor: z.string().min(1 , { message: 'Professor is missing' }),
+ professor: z.string().min(1, { message: 'Professor is missing' }),
term: z.enum(['Spring', 'Summer', 'Fall']),
- year: z.coerce.number({ message: 'Year is missing'}),
-})
+ year: z.coerce.number({ message: 'Year is missing' }),
+});
// Upload file to database w/ file metadata (Local)
export async function POST(req: Request) {
@@ -42,17 +42,11 @@ export async function POST(req: Request) {
const newFile = data.file as File;
if (!(newFile instanceof File)) {
- return NextResponse.json(
- { error: 'Invalid file upload' },
- { status: 400 },
- );
+ return NextResponse.json({ error: 'Invalid file upload' }, { status: 400 });
}
-
+
if (newFile.size === 0) {
- return NextResponse.json(
- { error: 'File is empty' },
- { status: 400 },
- );
+ return NextResponse.json({ error: 'File is empty' }, { status: 400 });
}
if (!allowedTypes.includes(newFile.type)) {
@@ -76,14 +70,11 @@ export async function POST(req: Request) {
});
if (!sectionData) {
- return NextResponse.json(
- { error: 'Section not found' },
- { status: 404 },
- );
+ return NextResponse.json({ error: 'Section not found' }, { status: 404 });
}
try {
- // Local file upload
+ // Local file upload - might need to change for production?
const uploadDir = path.join(process.cwd(), 'public', 'uploads');
await fs.promises.mkdir(uploadDir, { recursive: true });
@@ -96,10 +87,10 @@ export async function POST(req: Request) {
const fileMetadata = {
authorId: session.user.id,
sectionId: sectionData.id,
- fileTitle: newFile.name, // required by schema
- fileName: newFile.name, // required by schema
+ fileTitle: newFile.name, // required by schema
+ fileName: newFile.name, // required by schema
};
-
+
const result = await db.insert(file).values(fileMetadata).returning();
return NextResponse.json(
@@ -108,9 +99,6 @@ export async function POST(req: Request) {
);
} catch (err) {
console.error('File upload error:', err);
- return NextResponse.json(
- { error: 'File upload failed' },
- { status: 500 },
- );
+ return NextResponse.json({ error: 'File upload failed' }, { status: 500 });
}
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 657aae2..7303c67 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -6,6 +6,7 @@ import { Bai_Jamjuree, Inter } from 'next/font/google';
import { type Metadata } from 'next';
import { GoogleAnalytics } from '@next/third-parties/google';
import Link from 'next/link';
+import { AuthProvider } from './AuthProvider';
import theme from '@src/utils/theme';
import { ToastProvider } from '@src/components/toast/ToastProvider';
@@ -55,11 +56,13 @@ export default function RootLayout({
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
{process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' && (
)}
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index 652c2f2..0000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Metadata } from 'next';
-import NavBar from '@components/NavBar';
-
-export const metadata: Metadata = {
- alternates: {
- canonical: 'https://notebook.utdnebula.com',
- },
-};
-
-const Home = () => {
- return (
- <>
-
- >
- );
-};
-
-export default Home;
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
deleted file mode 100644
index ff31ef5..0000000
--- a/src/app/profile/page.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-'use client';
-
-import Avatar from '@mui/material/Avatar';
-import Link from 'next/link';
-
-export default function ProfilePage() {
- const user = {
- name: 'John Doe',
- handle: '@johndoe',
- email: 'johndoe@example.com',
- avatar: '/images/avatar.jpg',
- posts: 0,
- reports_submitted: 0,
- };
-
- return (
-
-
-
-
-
-
{user.name}
-
{user.handle}
-
{user.email}
-
-
- {user.posts} posts
-
-
- {user.reports_submitted}{' '}
- reports submitted
-
-
-
-
-
- Edit Profile
-
-
-
-
Saved Notes:
-
Uploaded Notes:
-
-
- );
-}
diff --git a/src/components/LoginButtons.tsx b/src/components/LoginButtons.tsx
new file mode 100644
index 0000000..52f6d81
--- /dev/null
+++ b/src/components/LoginButtons.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import { signIn, signOut, useSession } from 'next-auth/react';
+import { Button, Stack, Typography } from '@mui/material';
+
+export default function LoginButtons() {
+ const { data: session, status } = useSession();
+
+ if (status === 'loading') {
+ return Loading...;
+ }
+
+ // If logged in, show user info + logout
+ if (session?.user) {
+ return (
+
+
+ Logged in as {session.user.name ?? session.user.email}
+
+
+
+ );
+ }
+
+ // If not logged in, show Google & Discord buttons
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx
index 85b5d43..62096f1 100644
--- a/src/components/NavBar.tsx
+++ b/src/components/NavBar.tsx
@@ -1,7 +1,10 @@
+'use client';
+
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { IconButton, Tooltip } from '@mui/material';
+import { signOut } from 'next-auth/react';
export default function NavBar() {
return (
@@ -20,7 +23,12 @@ export default function NavBar() {
>
UTD Notebook
-
+
diff --git a/src/server/db/schema/file.ts b/src/server/db/schema/file.ts
index 7364ff5..d8e75ac 100644
--- a/src/server/db/schema/file.ts
+++ b/src/server/db/schema/file.ts
@@ -25,25 +25,21 @@ export const file = pgTable(
.notNull()
.references(() => user.id, { onDelete: 'cascade' }), // Could be 'set null' too, depending on what we want to do with it.
- sectionId: varchar('section_id', { length: 6 })
- .references(() => section.id, { onDelete: 'set null' }),
+ sectionId: varchar('section_id', { length: 6 }).references(
+ () => section.id,
+ { onDelete: 'set null' },
+ ),
- fileTitle: text('file_title')
- .notNull(),
+ fileTitle: text('file_title').notNull(),
- fileName: text('file_name')
- .notNull(),
+ fileName: text('file_name').notNull(),
publishDate: timestamp('publish_date', { withTimezone: true })
.notNull()
.defaultNow(),
- likes: integer('likes')
- .notNull()
- .default(0),
- saves: integer('saves')
- .notNull()
- .default(0),
+ likes: integer('likes').notNull().default(0),
+ saves: integer('saves').notNull().default(0),
// Edit flag for future workflows
edited: boolean('edited').notNull().default(false),
@@ -55,7 +51,7 @@ export const file = pgTable(
.notNull()
.defaultNow(),
},
- (t) => ([
+ (t) => [
// REVIEW: This is CASE SENSITIVE. File != file similar to Linux.
// So a user could have "Lecture 1 Notes" and "lecture 1 notes".
// I would recommend adding a PG extension for to support insensitivity.
@@ -66,7 +62,7 @@ export const file = pgTable(
check('file_likes_nonneg', sql`${t.likes} >= 0`),
check('file_saves_nonneg', sql`${t.saves} >= 0`),
- ]),
+ ],
);
export const fileRelations = relations(file, ({ one }) => ({
@@ -78,4 +74,4 @@ export const fileRelations = relations(file, ({ one }) => ({
fields: [file.sectionId],
references: [section.id],
}),
-}));
\ No newline at end of file
+}));
diff --git a/src/server/db/schema/section.ts b/src/server/db/schema/section.ts
index bd7244c..77dac66 100644
--- a/src/server/db/schema/section.ts
+++ b/src/server/db/schema/section.ts
@@ -22,27 +22,20 @@ export const section = pgTable(
.primaryKey(),
// "CS" or "CE", short and indexable
- prefix: varchar('prefix', { length: 4 })
- .notNull(),
+ prefix: varchar('prefix', { length: 4 }).notNull(),
// Course number like 1200
- number: varchar('number', { length: 4 })
- .notNull(),
+ number: varchar('number', { length: 4 }).notNull(),
// Section code like "001"
- sectionCode: varchar('section_code', { length: 3 })
- .notNull(),
+ sectionCode: varchar('section_code', { length: 3 }).notNull(),
// Semester split into term + year for better filtering
- term: termEnum('term')
- .notNull(),
- year: smallint('year')
- .notNull(),
+ term: termEnum('term').notNull(),
+ year: smallint('year').notNull(),
professor: text('professor'),
- numberOfNotes: integer('number_of_notes')
- .notNull()
- .default(0),
+ numberOfNotes: integer('number_of_notes').notNull().default(0),
// Not required, but good practice usually
createdAt: timestamp('created_at', { withTimezone: true })
@@ -52,14 +45,20 @@ export const section = pgTable(
.notNull()
.defaultNow(),
},
- (t) => ([
- uniqueIndex('section_unique_idx').on(t.prefix, t.number, t.sectionCode, t.term, t.year),
+ (t) => [
+ uniqueIndex('section_unique_idx').on(
+ t.prefix,
+ t.number,
+ t.sectionCode,
+ t.term,
+ t.year,
+ ),
index('section_by_course_idx').on(t.prefix, t.number),
index('section_by_professor_idx').on(t.professor),
index('section_by_semester_idx').on(t.term, t.year),
- ]),
+ ],
);
export const sectionRelations = relations(section, ({ many }) => ({
files: many(file),
-}));
\ No newline at end of file
+}));