diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index f5c66b9..2931fcf 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -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"; @@ -32,6 +33,7 @@ export default function DashboardPage() { const [isFilterOpen, setIsFilterOpen] = useState(false); const [accessToken, setAccessToken] = useState(null); const [tokenLoaded, setTokenLoaded] = useState(false); + const [isUploadOpen, setIsUploadOpen] = useState(false); const [notes, setNotes] = useState([]); const [page, setPage] = useState(1); @@ -40,6 +42,8 @@ export default function DashboardPage() { const [notesError, setNotesError] = useState(null); const [classesError, setClassesError] = useState(null); + const router = useRouter(); + useEffect(() => { const loadSession = async () => { const { data, error } = await supabase.auth.getSession(); @@ -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; @@ -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); @@ -181,10 +187,19 @@ export default function DashboardPage() { return (
+ {/* Header */}

Dashboard

+
+ {/* Class Selection */}
@@ -193,8 +208,10 @@ export default function DashboardPage() { className="border rounded px-2 py-1 flex items-center justify-between cursor-pointer bg-white" onClick={() => setIsClassDropdownOpen((open) => !open)} > - {selectedClassLabel} - + + {selectedClassLabel} + +
{isClassDropdownOpen && ( @@ -202,7 +219,7 @@ export default function DashboardPage() {
setClassSearch(e.target.value)} @@ -212,7 +229,7 @@ export default function DashboardPage() { {isFilterOpen && (
+ {/* Notes list */}
{classesError && ( -

{classesError}

+

{classesError}

)} {notesError && ( -

{notesError}

+

{notesError}

)}
    @@ -286,27 +305,33 @@ export default function DashboardPage() { {note.previewUrl ? ( ) : ( -
    +
    No preview
    )} -
    +
    {note.title}
    -
    +
    {note.profile_display_name ?? "Unknown uploader"} {" • "} {new Date(note.created_at).toLocaleDateString()}
    -
    - ↑ {note.upvote_count ?? 0} +
    + + ↑ {note.upvote_count ?? 0} + {" / "} - ↓ {note.downvote_count ?? 0} + + ↓ {note.downvote_count ?? 0} + {" • "} - Score: {note.score ?? 0} + + Score: {note.score ?? 0} +
    @@ -316,25 +341,25 @@ export default function DashboardPage() {
{loadingNotes && ( -

Loading notes…

+

Loading notes…

)} {!loadingNotes && hasMore && ( )} {!loadingNotes && !hasMore && notes.length > 0 && ( -

No more notes to load.

+

No more notes to load.

)} {!loadingNotes && notes.length === 0 && !notesError && ( -

No notes found.

+

No notes found.

)}
diff --git a/frontend/app/test-upload/page.tsx b/frontend/app/test-upload/page.tsx index f993cf5..a4fef17 100644 --- a/frontend/app/test-upload/page.tsx +++ b/frontend/app/test-upload/page.tsx @@ -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(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(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"); @@ -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)}`); diff --git a/frontend/app/upload/page.tsx b/frontend/app/upload/page.tsx new file mode 100644 index 0000000..0ed8a04 --- /dev/null +++ b/frontend/app/upload/page.tsx @@ -0,0 +1,281 @@ +// app/upload/page.tsx +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { supabase } from "@/lib/supabaseClient"; + +export default function UploadPage() { + const [classes, setClasses] = useState([]); + const [classSearch, setClassSearch] = useState(""); + const [classId, setClassId] = useState("all"); + const [isClassDropdownOpen, setIsClassDropdownOpen] = useState(false); + const [classesError, setClassesError] = useState(null); + + const [tokenLoaded, setTokenLoaded] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const router = useRouter(); + + const [file, setFile] = useState(null); + const [title, setTitle] = useState(""); + const [accessToken, setAccessToken] = useState(null); + const [result, setResult] = useState(null); + const [submitError, setSubmitError] = useState(null); + + // Authentications + useEffect(() => { + (async () => { + const { data, error } = await supabase.auth.getSession(); + if (error) { + console.log("UploadPage supabase.auth.getSession error:", error); + } + if (!data?.session) { + // not logged in + router.replace("/auth"); + return; + } + + setIsAuthenticated(true); + })(); + }, [router]); + + //recycled + useEffect(() => { + const loadSession = async () => { + const { data, error } = await supabase.auth.getSession(); + if (error) { + setClassesError("Not authenticated"); + } + setAccessToken(data.session?.access_token ?? null); + setTokenLoaded(true); + }; + loadSession(); + }, []); + + const selectedClassLabel = (() => { + if (!classId) return "--Select Class--"; + const c = classes.find((cl) => cl.id === classId); + if (!c) return "--Select Class--"; + return c.code ? `${c.name} (${c.code})` : c.name; + })(); + + const handleSelectClass = (id: string | "all") => { + console.log("handleSelectClass:", id); + setClassId(id); + setIsClassDropdownOpen(false); + setClassSearch(""); + }; + + const filteredClasses = useMemo(() => { + const term = classSearch.trim().toLowerCase(); + if (!term) return classes; + return classes.filter((c) => { + const label = (c.name + (c.code ? ` ${c.code}` : "")).toLowerCase(); + return label.includes(term); + }); + }, [classes, classSearch]); + + // + + useEffect(() => { + if (!tokenLoaded) return; + if (!accessToken) { + setClassesError("Not authenticated"); + return; + } + + const fetchClasses = async () => { + try { + const res = await fetch("/api/classes", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (!res.ok) { + const errorPayload = + (await res.json().catch(async () => ({ + error: await res.text().catch(() => ""), + }))) || {}; + setClassesError(errorPayload.error || "Failed to load classes"); + setClasses([]); + return; + } + const data = await res.json(); + setClasses(data.classes || []); + } catch (err) { + setClassesError("Failed to load classes"); + setClasses([]); + } + }; + + fetchClasses(); + }, [accessToken, tokenLoaded]); + + // File Upload + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitError(null); + setResult(null); + + if (!file) { + setSubmitError("No file selected"); + return; + } + + if (!classId || classId === "all") { + setSubmitError("Please select a class before uploading."); + return; + } + + if (!accessToken) { + setSubmitError("Missing access token. Please re-authenticate."); + return; + } + + const formData = new FormData(); + formData.append("file", file); + formData.append("class_id", classId); + formData.append("title", title); + + const res = await fetch("/api/upload", { + method: "POST", + body: formData, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + let payload: unknown; + try { + if (res.headers.get("content-type")?.includes("application/json")) { + payload = await res.json(); + } else { + const text = await res.text(); + payload = text ? { message: text } : null; + } + } catch (error) { + payload = { + error: "Response was not valid JSON", + details: String(error), + }; + } + + setResult(`${res.status}: ${JSON.stringify(payload)}`); + }; + + return ( +
+

Upload Notes

+ + {!isAuthenticated && ( +

+ Checking authentication… If you are not redirected, refresh the + page. +

+ )} + + {classesError && ( +

{classesError}

+ )} + +
+
+ + setFile(e.target.files?.[0] ?? null)} + /> +
+ +
+ {/* Class Selection */} +
+
+ + +
setIsClassDropdownOpen((open) => !open)} + > + + {selectedClassLabel} + + +
+ + {isClassDropdownOpen && ( +
+
+ setClassSearch(e.target.value)} + onClick={(e) => e.stopPropagation()} + /> +
+ + {filteredClasses.map((c) => ( + + ))} + + {filteredClasses.length === 0 && ( +
+ No classes match “{classSearch}” +
+ )} +
+ )} +
+
+
+ +
+ + setTitle(e.target.value)} + /> +
+ + {/* remove in production, kept for testing purposes */} + +
+ + setAccessToken(e.target.value)} + placeholder="Paste a user access token" + /> +
+ + {submitError && ( +

{submitError}

+ )} + + +
+ + {result && ( +
+          {result}
+        
+ )} +
+ ); +}