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
86 changes: 66 additions & 20 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import Link from "next/link";
import { createClient as createServerClient } from "../../lib/supabase/server";
import { Cinzel } from "next/font/google";
import checkUserCompletedQuizzes from "@/lib/checkUserCompletedQuizzes";
import { ArrowBigLeft, ArrowBigDown, ArrowBigRight } from "lucide-react";

const cinzel = Cinzel({
Expand All @@ -17,6 +17,36 @@ async function logout() {
redirect("/");
}

async function resetQuestProgress() {
"use server";
const supabase = await createServerClient();
const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
return;
}

// Delete all quest completions for this user
// The trigger will automatically update quests_completed count to 0
const { error: deleteError } = await supabase
.from("quest_completions")
.delete()
.eq("user_id", user.id);

if (deleteError) {
console.error("Error deleting quest completions:", deleteError);
}

// Revalidate the dashboard and profile pages to refresh data
revalidatePath("/dashboard");
revalidatePath("/profile");

// Redirect to refresh the page
redirect("/dashboard");
}

export default async function DashboardPage() {
const supabase = await createServerClient();
const {
Expand All @@ -25,19 +55,30 @@ export default async function DashboardPage() {

if (!user) redirect("/login");

// Completed quizzes
const completedQuizzes = await checkUserCompletedQuizzes();

const isHelloWorldComplete = completedQuizzes.has("hello-world");
const isVariablesComplete = completedQuizzes.has("variables");
const isUserInputComplete = completedQuizzes.has("user-input");
const isConditionalsComplete = completedQuizzes.has("conditionals");
const isLoopsComplete = completedQuizzes.has("loops");
const isMathComplete = completedQuizzes.has("math");
const isFunctionsComplete = completedQuizzes.has("functions");
const isListsArraysComplete = completedQuizzes.has("lists-arrays");
const isDictionaryComplete = completedQuizzes.has("dictionary");
const isRecursionComplete = completedQuizzes.has("recursion");
// Fetch individual quest completions from quest_completions table
const { data: questCompletions, error: completionsError } = await supabase
.from("quest_completions")
.select("quest_id")
.eq("user_id", user.id);

if (completionsError) {
console.error(`Error fetching quest completions: ${completionsError.message}`);
}

// Create a Set of completed quest IDs for quick lookup
const completedQuestIds = new Set(questCompletions?.map((qc) => qc.quest_id) || []);

// Check individual quest completion status
const isHelloWorldComplete = completedQuestIds.has("hello-world");
const isVariablesComplete = completedQuestIds.has("variables");
const isUserInputComplete = completedQuestIds.has("user-input");
const isConditionalsComplete = completedQuestIds.has("conditionals");
const isLoopsComplete = completedQuestIds.has("loops");
const isMathComplete = completedQuestIds.has("math");
const isFunctionsComplete = completedQuestIds.has("functions");
const isListsArraysComplete = completedQuestIds.has("lists-arrays");
const isDictionaryComplete = completedQuestIds.has("dictionary");
const isRecursionComplete = completedQuestIds.has("recursion");

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";
Expand Down Expand Up @@ -66,18 +107,23 @@ export default async function DashboardPage() {
>
<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}>
<span>Home</span>
</Link>
</div>
<h1 className="text-white text-6xl font-bold tracking-wider mb-4 drop-shadow-[0_0_15px_rgba(255,255,255,0.5)] hover:drop-shadow-[0_0_20px_rgba(255,255,255,0.7)] transition-all duration-300">
Dashboard
</h1>
</div>

<div className="flex items-center gap-4 p-0 w-fit">
<Link href="/" className={celestialButtonNoFullWidth}>
<span>Home</span>
</Link>
<Link href="/profile" className={celestialButtonNoFullWidth}>
<span>Profile</span>
</Link>
<form action={resetQuestProgress}>
<button type="submit" className={celestialButtonNoFullWidth}>
<span>Reset Quests</span>
</button>
</form>
<form action={logout}>
<button type="submit" className={celestialButtonNoFullWidth}>
<span>Log out</span>
Expand Down
21 changes: 13 additions & 8 deletions src/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function ProfilePage() {
// Fetch user profile from database
const { data: profile } = await supabase
.from('profiles')
.select('class')
.select('class, quests_completed')
.eq('id', user.id)
.maybeSingle(); // Use maybeSingle() instead of single() to handle case where profile doesn't exist

Expand All @@ -49,6 +49,11 @@ export default async function ProfilePage() {
})
: 'Unknown';

// Calculate Python progress based on quest completion
const totalQuests = 10;
const completedQuests = profile?.quests_completed ?? 0;
const pythonProgress = Math.round((completedQuests / totalQuests) * 100);

return (
<main className="relative min-h-dvh p-8">
{/* Background with blur */}
Expand Down Expand Up @@ -143,7 +148,7 @@ export default async function ProfilePage() {
Python
</span>
<span className="text-base" style={{ color: "#E0E0E0" }}>
85%
{pythonProgress}%
</span>
</div>
<div
Expand All @@ -153,7 +158,7 @@ export default async function ProfilePage() {
<div
className="h-full rounded-full"
style={{
width: "85%",
width: `${pythonProgress}%`,
background: "linear-gradient(to right, #be9661, #a7e4e7)",
}}
/>
Expand All @@ -167,7 +172,7 @@ export default async function ProfilePage() {
Java
</span>
<span className="text-base" style={{ color: "#E0E0E0" }}>
72%
0%
</span>
</div>
<div
Expand All @@ -177,7 +182,7 @@ export default async function ProfilePage() {
<div
className="h-full rounded-full"
style={{
width: "72%",
width: "0%",
background: "linear-gradient(to right, #a7e4e7, #be9661)",
}}
/>
Expand All @@ -191,7 +196,7 @@ export default async function ProfilePage() {
C++
</span>
<span className="text-base" style={{ color: "#E0E0E0" }}>
68%
0%
</span>
</div>
<div
Expand All @@ -201,7 +206,7 @@ export default async function ProfilePage() {
<div
className="h-full rounded-full"
style={{
width: "68%",
width: "0%",
background: "linear-gradient(135deg, #be9661, #a7e4e7, #be9661)",
}}
/>
Expand Down Expand Up @@ -284,7 +289,7 @@ export default async function ProfilePage() {
Quests
</span>
<span className="text-2xl font-bold" style={{ color: "#be9661" }}>
0/8
{profile?.quests_completed ?? 0}/10
</span>
</div>
</div>
Expand Down
22 changes: 14 additions & 8 deletions src/components/tutorial-quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ 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
// Updates database that user completed quiz
// Inserts into quest_completions table (triggers will auto-update quests_completed count)
const updateUserQuizProgress = async () => {
const supabase = createClient();

Expand All @@ -72,13 +71,20 @@ export default function Quiz({ quizData }: QuizProps) {
return;
}

const { error: insertError } = await supabase.from("user_quiz_progress").insert({
user_id: user.id,
quiz_id: quizData.id,
});
// Insert into quest_completions table
// The unique constraint prevents duplicates, and the trigger auto-updates quests_completed count
const { error: insertError } = await supabase
.from("quest_completions")
.insert({
user_id: user.id,
quest_id: quizData.id,
});

if (insertError) {
console.error(`Supabase insertion error: ${insertError}`);
// If it's a duplicate key error, the quest was already completed - that's okay
if (insertError.code !== "23505") {
console.error(`Error inserting quest completion: ${insertError.message}`);
}
}
};

Expand Down
45 changes: 45 additions & 0 deletions src/lib/updateQuestCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createClient } from "./supabase/client";

/**
* Updates the quests_completed count in the profiles table
* by counting distinct quiz_ids completed by the user
*/
export async function updateQuestCount() {
const supabase = createClient();

// Get user
const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
console.error("User not logged in, cannot update quest count.");
return;
}

// Count distinct quiz_ids for this user
const { data: quizData, error: selectError } = await supabase
.from("user_quiz_progress")
.select("quiz_id")
.eq("user_id", user.id);

if (selectError) {
console.error(`Error fetching quiz progress: ${selectError.message}`);
return;
}

// Get unique quiz IDs
const uniqueQuizIds = new Set(quizData?.map((row) => row.quiz_id) || []);
const questsCompleted = uniqueQuizIds.size;

// Update the profiles table
const { error: updateError } = await supabase
.from("profiles")
.update({ quests_completed: questsCompleted })
.eq("id", user.id);

if (updateError) {
console.error(`Error updating quest count: ${updateError.message}`);
}
}

Loading