Skip to content
Merged
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
79 changes: 52 additions & 27 deletions frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { supabase } from "@/lib/supabaseClient";
import PDFThumbnail from "@/app/components/pdf/PDFThumbnail";

Expand Down Expand Up @@ -32,6 +33,7 @@ export default function DashboardPage() {
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [tokenLoaded, setTokenLoaded] = useState(false);
const [isUploadOpen, setIsUploadOpen] = useState(false);

const [notes, setNotes] = useState<Note[]>([]);
const [page, setPage] = useState(1);
Expand All @@ -40,6 +42,8 @@ export default function DashboardPage() {
const [notesError, setNotesError] = useState<string | null>(null);
const [classesError, setClassesError] = useState<string | null>(null);

const router = useRouter();

useEffect(() => {
const loadSession = async () => {
const { data, error } = await supabase.auth.getSession();
Expand Down Expand Up @@ -68,8 +72,9 @@ export default function DashboardPage() {
});
if (!res.ok) {
const errorPayload =
(await res.json().catch(async () => ({ error: await res.text().catch(() => "") }))) ||
{};
(await res.json().catch(async () => ({
error: await res.text().catch(() => ""),
}))) || {};
setClassesError(errorPayload.error || "Failed to load classes");
setClasses([]);
return;
Expand Down Expand Up @@ -119,8 +124,9 @@ export default function DashboardPage() {
});
if (!res.ok) {
const errorPayload =
(await res.json().catch(async () => ({ error: await res.text().catch(() => "") }))) ||
{};
(await res.json().catch(async () => ({
error: await res.text().catch(() => ""),
}))) || {};
setNotesError(errorPayload.error || "Failed to fetch notes");
setHasMore(false);
setLoadingNotes(false);
Expand Down Expand Up @@ -181,10 +187,19 @@ export default function DashboardPage() {

return (
<main className="p-6 space-y-6">
{/* Header */}
<header className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">Dashboard</h1>
<button
type="button"
className="border rounded px-3 py-1 text-sm bg-white text-gray-800"
onClick={() => router.replace("/upload")}
>
{isUploadOpen ? "Cancel" : "Upload"}
</button>
</header>

{/* Class Selection */}
<section className="flex flex-wrap gap-4 items-center">
<div className="relative min-w-[220px]">
<label className="block text-sm mb-1">Class</label>
Expand All @@ -193,16 +208,18 @@ export default function DashboardPage() {
className="border rounded px-2 py-1 flex items-center justify-between cursor-pointer bg-white"
onClick={() => setIsClassDropdownOpen((open) => !open)}
>
<span className="text-sm truncate">{selectedClassLabel}</span>
<span className="ml-2 text-xs">▾</span>
<span className="text-sm truncate text-gray-800">
{selectedClassLabel}
</span>
<span className="ml-2 text-xs text-gray-700">▾</span>
</div>

{isClassDropdownOpen && (
<div className="absolute z-10 mt-1 w-full border rounded bg-white shadow-md max-h-64 overflow-y-auto">
<div className="p-2 border-b">
<input
type="text"
className="w-full border px-2 py-1 text-sm"
className="w-full border px-2 py-1 text-sm text-gray-800 placeholder:text-gray-500"
placeholder="Search classes…"
value={classSearch}
onChange={(e) => setClassSearch(e.target.value)}
Expand All @@ -212,7 +229,7 @@ export default function DashboardPage() {

<button
type="button"
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
className="w-full text-left px-3 py-2 text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleSelectClass("all")}
>
All classes
Expand All @@ -222,7 +239,7 @@ export default function DashboardPage() {
<button
key={c.id}
type="button"
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
className="w-full text-left px-3 py-2 text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleSelectClass(c.id)}
>
{c.name}
Expand All @@ -231,35 +248,36 @@ export default function DashboardPage() {
))}

{filteredClasses.length === 0 && (
<div className="px-3 py-2 text-xs text-gray-500">
<div className="px-3 py-2 text-xs text-gray-700">
No classes match “{classSearch}”
</div>
)}
</div>
)}
</div>

{/* Filter Selection */}
<div className="relative">
<label className="block text-sm mb-1">Sort</label>
<button
type="button"
className="border rounded px-3 py-1 text-sm bg-white"
className="border rounded px-3 py-1 text-sm bg-white text-gray-800"
onClick={() => setIsFilterOpen((open) => !open)}
>
Filters
Filters <span className="text-gray-700">▾</span>
</button>
{isFilterOpen && (
<div className="absolute z-10 mt-1 w-40 border rounded bg-white shadow-md overflow-hidden">
<button
type="button"
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
className="w-full text-left px-3 py-2 text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleSelectSort("newest")}
>
Newest
</button>
<button
type="button"
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100"
className="w-full text-left px-3 py-2 text-sm text-gray-800 hover:bg-gray-100"
onClick={() => handleSelectSort("oldest")}
>
Oldest
Expand All @@ -269,13 +287,14 @@ export default function DashboardPage() {
</div>
</section>

{/* Notes list */}
<section>
{classesError && (
<p className="text-sm text-red-600 mb-2">{classesError}</p>
<p className="text-sm text-red-500 mb-2">{classesError}</p>
)}

{notesError && (
<p className="text-sm text-red-600 mb-2">{notesError}</p>
<p className="text-sm text-red-500 mb-2">{notesError}</p>
)}

<ul className="divide-y">
Expand All @@ -286,27 +305,33 @@ export default function DashboardPage() {
{note.previewUrl ? (
<PDFThumbnail fileUrl={note.previewUrl} width={200} />
) : (
<div className="w-[200px] h-[280px] bg-gray-100 border rounded flex items-center justify-center text-xs text-gray-400">
<div className="w-[200px] h-[280px] bg-gray-100 border rounded flex items-center justify-center text-xs text-gray-600">
No preview
</div>
)}
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-black/50 text-white px-3 py-2 flex flex-col justify-end gap-1 rounded-b">
<div className="absolute inset-x-0 bottom-0 h-1/3 bg-black/60 text-white px-3 py-2 flex flex-col justify-end gap-1 rounded-b">
<div className="text-sm font-semibold leading-tight line-clamp-2">
{note.title}
</div>
<div className="text-[11px] text-gray-200 leading-tight">
<div className="text-[11px] text-gray-100 leading-tight">
<span className="font-medium">
{note.profile_display_name ?? "Unknown uploader"}
</span>
{" • "}
{new Date(note.created_at).toLocaleDateString()}
</div>
<div className="text-[11px]">
<span className="text-green-200 font-semibold">↑ {note.upvote_count ?? 0}</span>
<div className="text-[11px] text-gray-100">
<span className="text-green-200 font-semibold">
↑ {note.upvote_count ?? 0}
</span>
{" / "}
<span className="text-red-200 font-semibold">↓ {note.downvote_count ?? 0}</span>
<span className="text-red-200 font-semibold">
↓ {note.downvote_count ?? 0}
</span>
{" • "}
<span className="text-gray-100 font-semibold">Score: {note.score ?? 0}</span>
<span className="font-semibold">
Score: {note.score ?? 0}
</span>
</div>
</div>
</div>
Expand All @@ -316,25 +341,25 @@ export default function DashboardPage() {
</ul>

{loadingNotes && (
<p className="mt-3 text-sm text-gray-500">Loading notes…</p>
<p className="mt-3 text-sm text-gray-600">Loading notes…</p>
)}

{!loadingNotes && hasMore && (
<button
type="button"
onClick={handleLoadMore}
className="mt-4 border rounded px-3 py-1 text-sm"
className="mt-4 border rounded px-3 py-1 text-sm text-gray-800 bg-white"
>
Load more
</button>
)}

{!loadingNotes && !hasMore && notes.length > 0 && (
<p className="mt-3 text-xs text-gray-400">No more notes to load.</p>
<p className="mt-3 text-xs text-gray-600">No more notes to load.</p>
)}

{!loadingNotes && notes.length === 0 && !notesError && (
<p className="mt-3 text-sm text-gray-500">No notes found.</p>
<p className="mt-3 text-sm text-gray-600">No notes found.</p>
)}
</section>
</main>
Expand Down
35 changes: 32 additions & 3 deletions frontend/app/test-upload/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
// app/test-upload/page.tsx
"use client";

import { useState } from "react";
import type { Session } from "@supabase/supabase-js";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { supabase } from "@/lib/supabaseClient";

export default function TestUploadPage() {
const router = useRouter();
const [ isAuthenticated, setIsAuthenticated] = useState(false);

const [file, setFile] = useState<File | null>(null);
const [classId, setClassId] = useState("");
const [title, setTitle] = useState("");
const [accessToken, setAccessToken] = useState(
process.env.NEXT_PUBLIC_TEST_ACCESS_TOKEN ?? "",
process.env.NEXT_PUBLIC_TEST_ACCESS_TOKEN ?? ""
);
const [result, setResult] = useState<string | null>(null);

// Authentications
useEffect(() => {
(async () => {
const { data, error } = await supabase.auth.getSession();
if (error) {
// optional: log
console.log("testUploadPage supabase.auth.getSession error:");
console.log("\t" + error);
}
if (!data?.session) { // not logged in
router.replace("/auth");
return;
}

setIsAuthenticated(true);
})();
}, [router]);

// File Upload
const handleSubmit = async (e: React.FormEvent) => {

e.preventDefault();
if (!file) {
setResult("No file selected");
Expand Down Expand Up @@ -43,7 +69,10 @@ export default function TestUploadPage() {
payload = text ? { message: text } : null;
}
} catch (error) {
payload = { error: "Response was not valid JSON", details: String(error) };
payload = {
error: "Response was not valid JSON",
details: String(error),
};
}

setResult(`${res.status}: ${JSON.stringify(payload)}`);
Expand Down
Loading