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
42 changes: 33 additions & 9 deletions src/app/dashboard/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
}));

Expand Down Expand Up @@ -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");
});
});
53 changes: 37 additions & 16 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -26,34 +27,42 @@ 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 (
<main
className={`relative min-h-dvh p-8 text-white ${cinzel.className}`}
style={{
backgroundImage: "url('/dashboard.png')",
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<div className="flex justify-between items-start w-full">

<div className="flex flex-col gap-4 p-0 w-fit">
<h1 className="text-white text-5xl font-bold tracking-wider mb-4">
Dashboard
</h1>
<div className="flex flex-col gap-4 w-32">
<div className="flex justify-between items-start w-full">
<div className="flex flex-col gap-4 p-0 w-fit">
<h1 className="text-white text-5xl font-bold tracking-wider mb-4">Dashboard</h1>
<div className="flex flex-col gap-4 w-32">
<Link href="/" className={celestialButtonClasses} aria-label="Go home">
<span>Home</span>
</Link>
</div>
</div>

{/* profile & logout */}
<div className="flex items-center gap-4 p-0 w-fit">
<div className="flex items-center gap-4 p-0 w-fit">
<Link href="/profile" className={celestialButtonNoFullWidth} aria-label="Go to profile">
<span>Profile</span>
</Link>
Expand All @@ -66,11 +75,23 @@ export default async function DashboardPage() {
</div>

{/* tutorials */}
<div className="absolute bottom-16 left-1/2 -translate-x-1/2">
<Link href="tutorial-hello-world" className={celestialButtonNoFullWidth}>
<div className="absolute bottom-16 left-1/2 -translate-x-1/2">
<div className="flex flex-col gap-16">
<Link
href="tutorial-variables"
className={isVariablesComplete ? greenButtonClass : greyButtonClass}
>
Variables
</Link>

<Link
href="tutorial-hello-world"
className={isHelloWorldComplete ? greenButtonClass : greyButtonClass}
>
Hello World
</Link>
</div>
</div>
</main>
);
}
}
50 changes: 50 additions & 0 deletions src/app/tutorial-variables/page.tsx
Original file line number Diff line number Diff line change
@@ -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
<div
className="min-h-screen p-4 md:p-12"
style={{
backgroundImage: "url('/geminiblurred.png')",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundColor: "#fef3c7",
}}
>
<div className="inline-block p-4" style={{ zIndex: 10 }}>
<BackToDashBoardLink />
</div>

{/* "scroll"*/}
<article
className={`max-w-4xl mx-auto bg-amber-100 p-8 md:p-12 shadow-2xl shadow-amber-950/70 space-y-8
${bodyFontClass} border border-amber-800 transform rotate-[-0.5deg]
rounded-t-[4rem] rounded-b-lg`}
>
{/* title*/}
<h1
className={`text-4xl md:text-5xl font-bold ${cinzelTitleClass}
border-b-4 border-amber-900 pb-4 mb-8 text-center uppercase`}
>
Quest: The Artisan&apos;s Toolkit - Mastering the Variable Vaults
</h1>

<hr className="my-8 border-amber-900/50" />
</article>
</div>
);
}
30 changes: 30 additions & 0 deletions src/components/tutorial-quiz.tsx
Original file line number Diff line number Diff line change
@@ -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"] });

Expand Down Expand Up @@ -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
};
Expand All @@ -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 (
<div className="mt-12 p-8 border-2 border-amber-800/50 rounded-xl bg-amber-50/50 shadow-inner shadow-amber-900/20">
<h2 className={`text-3xl font-bold text-center mb-6 text-amber-900 ${cinzel.className}`}>
Expand Down
1 change: 1 addition & 0 deletions src/data/quizzes/01-hello-world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down
25 changes: 25 additions & 0 deletions src/lib/checkUserCompletedQuizzes.ts
Original file line number Diff line number Diff line change
@@ -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));
}
1 change: 1 addition & 0 deletions src/lib/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export type Question = {

export type QuizData = {
title: string;
id: string;
questions: Question[];
};