Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
23 changes: 23 additions & 0 deletions src/app/(auth)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import LoginButtons from '@src/components/LoginButtons';
import { getServerAuthSession } from '@src/server/auth';
import { redirect } from 'next/navigation';

const Login = async () => {
const session = await getServerAuthSession();

// If logged in, go straight to homepage
if (session?.user) {
redirect('/homepage');
}

return (
<main className="flex min-h-screen flex-col items-center justify-center gap-6">
<h1 className="text-3xl font-bold text-white">UTD Notebook</h1>
<p className="text-white">Sign in to continue</p>

<LoginButtons />
</main>
);
};

export default Login;
20 changes: 20 additions & 0 deletions src/app/(main)/homepage/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { getServerAuthSession } from '@src/server/auth';
export const metadata: Metadata = {
alternates: {
canonical: 'https://notebook.utdnebula.com',
},
};

const Home = async () => {
const session = await getServerAuthSession();

// If not logged in, go to login page
if (!session?.user) {
redirect('/');
}
return <></>;
};

export default Home;
12 changes: 12 additions & 0 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// src/app/(main)/layout.tsx
import type { ReactNode } from 'react';
import NavBar from '@src/components/NavBar';

export default function MainLayout({ children }: { children: ReactNode }) {
return (
<>
<NavBar />
{children}
</>
);
}
112 changes: 112 additions & 0 deletions src/app/(main)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { getServerAuthSession } from '@src/server/auth';

import Avatar from '@mui/material/Avatar';
import Link from 'next/link';
import UploadNoteForm from '../uploadNotes/uploadNotes';
import { db } from '@src/server/db';
import { eq } from 'drizzle-orm';

export default async function ProfilePage() {
const session = await getServerAuthSession();

if (!session || !session.user) {
return (
<main className="min-h-screen p-8">
<div className="mx-auto max-w-4xl">
<h1 className="text-2xl font-semibold text-white">Unauthorized</h1>
<p className="mt-4 text-white">
You must be logged in to view this page.
</p>
</div>
</main>
);
}

const uploadedFiles = await db.query.file.findMany({
where: (f) => eq(f.authorId, session.user.id),
with: {
section: true,
},
orderBy: (f, { desc }) => [desc(f.createdAt)],
});

const user = {
name: session?.user?.name ?? 'John Doe',
handle: '@johndoe',
email: session?.user?.email ?? '[email protected]',
avatar: session?.user?.image ?? '/images/avatar.jpg',
posts: 0,
reports_submitted: 0,
};

return (
<main className="min-h-screen p-8">
<div className="mx-auto max-w-4xl">
<div className="flex items-center gap-6">
<Avatar
alt={user.name}
src={user.avatar}
sx={{ width: 96, height: 96 }}
/>
<div>
<h1 className="text-2xl font-semibold">{user.name}</h1>
<div className="text-sm text-white">{user.handle}</div>
<p className="mt-2 text-white">{user.email}</p>
<div className="mt-4 flex gap-4 text-sm text-white">
<div>
<strong className="font-bold">{user.posts}</strong> posts
</div>
<div>
<strong className="font-bold">{user.reports_submitted}</strong>{' '}
reports submitted
</div>
</div>
</div>
<div className="ml-auto flex items-center gap-3">
<Link
href="/profile/edit"
className="rounded-md px-4 py-2 text-white"
>
Edit Profile
</Link>
</div>
</div>
<h2 className="mt-10 text-xl font-semibold">Saved Notes:</h2>
<h2 className="mt-10 text-xl font-semibold">Uploaded Notes:</h2>
{uploadedFiles.length === 0 ? (
<p className="mt-2 text-sm text-white/60">
You haven&apos;t uploaded any notes yet.
</p>
) : (
<ul className="mt-4 space-y-2">
{uploadedFiles.map((f) => (
<li
key={f.id}
className="flex items-center justify-between rounded-md bg-white/5 px-3 py-2 text-sm"
>
<div>
<div className="font-medium">{f.fileTitle}</div>
<div className="text-xs text-white/60">
{f.section
? `${f.section.prefix} ${f.section.number}.${f.section.sectionCode}`
: 'N/A'}
• Uploaded {new Date(f.createdAt).toLocaleString()}
</div>
</div>
{/* This gets from local - might need to change for production release */}
<Link
href={`/uploads/${f.fileName}`}
target="_blank"
className="text-xs underline"
>
View
</Link>
</li>
))}
</ul>
)}
<UploadNoteForm />
</div>
</main>
);
}
142 changes: 142 additions & 0 deletions src/app/(main)/uploadNotes/uploadNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use client';

import React, { useState } from 'react';
import {
Box,
Button,
TextField,
MenuItem,
Typography,
Paper,
} from '@mui/material';
import { useRouter } from 'next/navigation';
export default function UploadNoteForm() {
const router = useRouter();
const [file, setFile] = useState<File | null>(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 (
<Paper sx={{ p: 3, mt: 3, backgroundColor: '#1E1E1E' }}>
<Typography variant="h6" color="white" gutterBottom>
Upload a New Note
</Typography>

<Box display="flex" flexDirection="column" gap={2}>
<Button variant="contained" component="label">
{file ? file.name : 'Choose File'}
<input
type="file"
hidden
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
</Button>

<TextField
label="Prefix (e.g. CS)"
value={prefix}
onChange={(e) => setPrefix(e.target.value)}
/>

<TextField
label="Course Number (e.g. 1337)"
value={courseNumber}
onChange={(e) => setCourseNumber(e.target.value)}
/>

<TextField
label="Section Code (e.g. 001)"
value={sectionCode}
onChange={(e) => setSectionCode(e.target.value)}
/>

<TextField
label="Professor"
value={professor}
onChange={(e) => setProfessor(e.target.value)}
/>

<TextField
select
label="Term"
value={term}
onChange={(e) => setTerm(e.target.value)}
>
{termOptions.map((t) => (
<MenuItem key={t} value={t}>
{t}
</MenuItem>
))}
</TextField>

<TextField
label="Year"
type="number"
value={year}
onChange={(e) => setYear(e.target.value)}
/>

<Button
variant="contained"
color="primary"
onClick={() => void handleUpload()}
>
Upload Note
</Button>
</Box>
</Paper>
);
}
8 changes: 8 additions & 0 deletions src/app/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use client';

import { SessionProvider } from 'next-auth/react';
import type { ReactNode } from 'react';

export function AuthProvider({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
Loading