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"); + }); }); diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 6ab4ef7..5819955 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,23 @@ export default async function DashboardPage() {
{/* tutorials */} -
- +
+
+ + Variables + + + Hello World
+
); -} \ No newline at end of file +} 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 +

+ +
+
+
+ ); +} diff --git a/src/components/tutorial-quiz.tsx b/src/components/tutorial-quiz.tsx index 6edc5f1..43b79e6 100644 --- a/src/components/tutorial-quiz.tsx +++ b/src/components/tutorial-quiz.tsx @@ -1,6 +1,9 @@ +"use client"; + import { Cinzel } from "next/font/google"; import { useState } from "react"; import { QuizData } from "@/lib/types/types"; +import { createClient } from "@/lib/supabase/client"; const cinzel = Cinzel({ subsets: ["latin"], weight: ["700"] }); @@ -40,6 +43,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 +56,32 @@ export default function Quiz({ quizData }: QuizProps) { setIsCorrect(null); }; + // 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(); + + // 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/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)); +} 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[]; };