From 0d00729833967bcda86288e1b22f2afe027d7e23 Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Sun, 23 Nov 2025 21:20:59 -0500 Subject: [PATCH 1/6] feat: add and connect database for quiz progress tracking --- src/components/tutorial-quiz.tsx | 29 +++++++++++++++++++++++++++++ src/data/quizzes/01-hello-world.ts | 1 + src/lib/types/types.ts | 1 + 3 files changed, 31 insertions(+) diff --git a/src/components/tutorial-quiz.tsx b/src/components/tutorial-quiz.tsx index 6edc5f1..90276d7 100644 --- a/src/components/tutorial-quiz.tsx +++ b/src/components/tutorial-quiz.tsx @@ -1,6 +1,10 @@ +"use client"; + import { Cinzel } from "next/font/google"; import { useState } from "react"; import { QuizData } from "@/lib/types/types"; +import { createClient } from "@/lib/supabase/client"; +// Idk if to use from server, or client const cinzel = Cinzel({ subsets: ["latin"], weight: ["700"] }); @@ -40,6 +44,7 @@ export default function Quiz({ quizData }: QuizProps) { setIsCorrect(null); } else { setShowResults(true); + updateUserQuizProgress(); } }, 2000); // 2 second delay so they can read the feedback }; @@ -52,6 +57,30 @@ export default function Quiz({ quizData }: QuizProps) { setIsCorrect(null); }; + // Updates database that user completed quiz, no score to keep things simple + const updateUserQuizProgress = async () => { + const supabase = createClient(); + + // Get user + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + console.error("User not logged in, cannot save progress."); + return; + } + + const { error: insertError } = await supabase.from("user_quiz_progress").insert({ + user_id: user.id, + quiz_id: quizData.id, + }); + + if (insertError) { + console.error(`Supabase insertion error: ${insertError}`); + } + }; + return (

diff --git a/src/data/quizzes/01-hello-world.ts b/src/data/quizzes/01-hello-world.ts index 225247b..a5a92af 100644 --- a/src/data/quizzes/01-hello-world.ts +++ b/src/data/quizzes/01-hello-world.ts @@ -2,6 +2,7 @@ import { QuizData } from "@/lib/types/types"; export const helloWorldQuiz: QuizData = { title: "The Oracle's First Greeting", + id: "hello-world", questions: [ { questionText: "What is the primary purpose of a 'Hello World' program?", diff --git a/src/lib/types/types.ts b/src/lib/types/types.ts index 03883c5..f09bfd3 100644 --- a/src/lib/types/types.ts +++ b/src/lib/types/types.ts @@ -6,5 +6,6 @@ export type Question = { export type QuizData = { title: string; + id: string; questions: Question[]; }; From e0b43f5e343641136d7cb124753b1e6f71143002 Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Sun, 23 Nov 2025 21:53:41 -0500 Subject: [PATCH 2/6] feat: add checkUserCompletedQuizzes.ts --- src/lib/checkUserCompletedQuizzes.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/lib/checkUserCompletedQuizzes.ts diff --git a/src/lib/checkUserCompletedQuizzes.ts b/src/lib/checkUserCompletedQuizzes.ts new file mode 100644 index 0000000..ac726ac --- /dev/null +++ b/src/lib/checkUserCompletedQuizzes.ts @@ -0,0 +1,25 @@ +import { createClient } from "./supabase/server"; + +export default async function checkUserCompletedQuizzes() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + console.error("User not logged in, cannot save progress."); + return new Set(); + } + + const { data: quizData, error: selectError } = await supabase + .from("user_quiz_progress") + .select("quiz_id") + .eq("user_id", user.id); + + if (selectError) { + console.error(`Supabase selection error: ${selectError.message}`); + return new Set(); + } + + return new Set(quizData.map((row) => row.quiz_id)); +} From 3e740420797a30b5e79a5e34f42e4906c7cb59a1 Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Sun, 23 Nov 2025 22:05:09 -0500 Subject: [PATCH 3/6] feat: add green and grey for quiz completion --- src/app/dashboard/page.tsx | 50 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 6ab4ef7..d29f38b 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,7 +1,8 @@ import { redirect } from "next/navigation"; import Link from "next/link"; import { createClient as createServerClient } from "../../lib/supabase/server"; -import { Cinzel } from 'next/font/google'; // Import Cinzel font +import { Cinzel } from "next/font/google"; // Import Cinzel font +import checkUserCompletedQuizzes from "@/lib/checkUserCompletedQuizzes"; //font for words const cinzel = Cinzel({ @@ -26,26 +27,34 @@ export default async function DashboardPage() { redirect("/login"); } - const celestialButtonClasses = "btn border-2 border-cyan-400 text-cyan-400 bg-transparent hover:bg-cyan-900/50 hover:border-cyan-200 hover:text-cyan-200 shadow-lg shadow-cyan-500/50 transition duration-300 ease-in-out w-full"; - const celestialButtonNoFullWidth = "btn border-2 border-cyan-400 text-cyan-400 bg-transparent hover:bg-cyan-900/50 hover:border-cyan-200 hover:text-cyan-200 shadow-lg shadow-cyan-500/50 transition duration-300 ease-in-out"; + // Check which quizzes has the user completed + const completedQuizzes = await checkUserCompletedQuizzes(); + const isHelloWorldComplete = completedQuizzes.has("hello-world"); + const isVariablesComplete = completedQuizzes.has("variables"); + + const celestialButtonClasses = + "btn border-2 border-cyan-400 text-cyan-400 bg-transparent hover:bg-cyan-900/50 hover:border-cyan-200 hover:text-cyan-200 shadow-lg shadow-cyan-500/50 transition duration-300 ease-in-out w-full"; + const celestialButtonNoFullWidth = + "btn border-2 border-cyan-400 text-cyan-400 bg-transparent hover:bg-cyan-900/50 hover:border-cyan-200 hover:text-cyan-200 shadow-lg shadow-cyan-500/50 transition duration-300 ease-in-out"; + const greyButtonClass = + "btn border-2 border-gray-400 text-gray-400 bg-transparent hover:bg-gray-900/50 hover:border-gray-200 hover:text-gray-200 shadow-lg shadow-gray-500/50 transition duration-300 ease-in-out"; + const greenButtonClass = + "btn border-2 border-emerald-400 text-emerald-400 bg-transparent hover:bg-emerald-900/50 hover:border-emerald-200 hover:text-emerald-200 shadow-lg shadow-emerald-500/50 transition duration-300 ease-in-out"; return (
-
- -
-

- Dashboard -

-
+
+
+

Dashboard

+
Home @@ -53,7 +62,7 @@ export default async function DashboardPage() {
{/* profile & logout */} -
+
Profile @@ -66,11 +75,14 @@ export default async function DashboardPage() {
{/* tutorials */} -
- - Hello World - -
+
+ + Hello World + +
); -} \ No newline at end of file +} From 0c5a4037a256427d3066bc8286aab1847c718b33 Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Sun, 23 Nov 2025 22:15:42 -0500 Subject: [PATCH 4/6] feat: add variables placeholder page to test greyness --- src/app/dashboard/page.tsx | 21 ++++++++---- src/app/tutorial-variables/page.tsx | 50 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 src/app/tutorial-variables/page.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d29f38b..5819955 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -76,12 +76,21 @@ export default async function DashboardPage() { {/* tutorials */}
- - Hello World - +
+ + Variables + + + + Hello World + +
); diff --git a/src/app/tutorial-variables/page.tsx b/src/app/tutorial-variables/page.tsx new file mode 100644 index 0000000..238be18 --- /dev/null +++ b/src/app/tutorial-variables/page.tsx @@ -0,0 +1,50 @@ +"use client"; + +import BackToDashBoardLink from "@/components/back-to-dashboard-link"; +import { Cinzel } from "next/font/google"; + +const cinzel = Cinzel({ + subsets: ["latin"], + weight: ["400", "700"], +}); + +// body text (Times New Roman= more readable) +const bodyFontClass = "font-serif text-amber-950"; +// titles (Cinzel font) +const cinzelTitleClass = cinzel.className; + +export default function TutorialHelloWorld() { + return ( + // Background of scroll +
+
+ +
+ + {/* "scroll"*/} +
+ {/* title*/} +

+ Quest: The Artisan's Toolkit - Mastering the Variable Vaults +

+ +
+
+
+ ); +} From 6e7fb719ab254249cbb831bcd375a63441ecab4e Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Sun, 23 Nov 2025 22:23:23 -0500 Subject: [PATCH 5/6] docs: write comments on TODO --- src/components/tutorial-quiz.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/tutorial-quiz.tsx b/src/components/tutorial-quiz.tsx index 90276d7..43b79e6 100644 --- a/src/components/tutorial-quiz.tsx +++ b/src/components/tutorial-quiz.tsx @@ -4,7 +4,6 @@ import { Cinzel } from "next/font/google"; import { useState } from "react"; import { QuizData } from "@/lib/types/types"; import { createClient } from "@/lib/supabase/client"; -// Idk if to use from server, or client const cinzel = Cinzel({ subsets: ["latin"], weight: ["700"] }); @@ -58,6 +57,8 @@ export default function Quiz({ quizData }: QuizProps) { }; // Updates database that user completed quiz, no score to keep things simple + // TODO: Update the insert with a upsert, and keep track of the user's most recent score on quiz + // Also another neat feature would be loading the quiz result instead of resetting the quiz each time const updateUserQuizProgress = async () => { const supabase = createClient(); From f416f9587d263da69cb8d8d34d080d9be118d0fc Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Mon, 24 Nov 2025 00:15:18 -0500 Subject: [PATCH 6/6] test: update mock, add new test for green button check --- src/app/dashboard/__tests__/page.test.tsx | 42 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/app/dashboard/__tests__/page.test.tsx b/src/app/dashboard/__tests__/page.test.tsx index e59f7a9..c727472 100644 --- a/src/app/dashboard/__tests__/page.test.tsx +++ b/src/app/dashboard/__tests__/page.test.tsx @@ -3,32 +3,41 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect, beforeEach } from "vitest"; import DashboardPage from "../page"; -// Mock next/navigation to prevent redirect calls +// Mock next/navigation vi.mock("next/navigation", () => ({ redirect: vi.fn(), })); // Mock next/font/google vi.mock("next/font/google", () => ({ - Cinzel: () => ({ - className: "mocked-cinzel-font", - }), + Cinzel: () => ({ className: "mocked-cinzel-font" }), })); -// Mock Supabase server client to return a fake authenticated user +// Mock for .from().select().eq() +const mockFrom = vi.fn(); +const mockSelect = vi.fn(); +const mockEq = vi.fn(); + +mockFrom.mockReturnValue({ + select: mockSelect.mockReturnValue({ + eq: mockEq.mockResolvedValue({ + data: [], // Default: No completed quizzes + error: null, + }), + }), +}); + vi.mock("../../../lib/supabase/server", () => ({ createClient: vi.fn(async () => ({ auth: { getUser: vi.fn(async () => ({ data: { - user: { - email: "test@example.com", - id: "test-user-id", - }, + user: { email: "test@example.com", id: "test-user-id" }, }, })), signOut: vi.fn(), }, + from: mockFrom, })), })); @@ -66,4 +75,19 @@ describe("Dashboard Page Tests", () => { render(page); expect(screen.getByRole("link", { name: /Profile/i })).toBeInTheDocument(); }); + + it("should show Green button for Hello World if completed", async () => { + // Override the mock for THIS test to simulate completion + mockEq.mockResolvedValue({ + data: [{ quiz_id: "hello-world" }], + error: null, + }); + + const page = await DashboardPage(); + render(page); + + const link = screen.getByRole("link", { name: /Hello World/i }); + // Check if link is green + expect(link.className).toContain("text-emerald-400"); + }); });