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
7 changes: 6 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "./globals.css";
import { ToastProvider } from "@/components/ui/ToastProvider";
import StoreProvider from "@/providers/storeProvider";
import ClientLayout from "@/components/ClientLayout";
import CompletionFeatureProvider from "@/providers/CompletionFeatureProvider";

const poppins = Poppins({
subsets: ["latin"],
Expand Down Expand Up @@ -32,7 +33,11 @@ export default function RootLayout({
>
<StoreProvider>
<ToastProvider>
<ClientLayout>{children}</ClientLayout>
<CompletionFeatureProvider>
<ClientLayout>
{children}
</ClientLayout>
</CompletionFeatureProvider>
</ToastProvider>
</StoreProvider>
</body>
Expand Down
30 changes: 18 additions & 12 deletions frontend/app/quiz/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"use client";
import { useRef, useEffect } from "react";

import { useState, useRef, useEffect } from "react";
import { Nunito } from "next/font/google";
import { MOCK_QUIZ } from "@/lib/Quiz_data";
import { QuizHeader } from "@/components/quiz/QuizHeader";
import { AnswerOption } from "@/components/quiz/AnswerOption";
import { LevelComplete } from "@/components/quiz/LevelComplete";
import { QuizCompletionStats } from "../../components/quiz/QuizCompletionStats";
import { useQuiz } from "../../hooks/useQuiz";
import { useAppSelector } from "../../lib/reduxHooks";
import { QuizHeader } from "../../components/quiz/QuizHeader";
import { AnswerOption } from "../../components/quiz/AnswerOption";
import { LevelComplete } from "../../components/quiz/LevelComplete";

const nunito = Nunito({
subsets: ["latin"],
Expand Down Expand Up @@ -114,15 +117,18 @@ export default function QuizPage() {
/>
)}

<main className="flex-grow flex flex-col items-center justify-center max-w-[566px] mx-auto w-full">
<main className="grow flex flex-col items-center justify-center max-w-[566px] mx-auto w-full">
{isFinished ? (
<LevelComplete
totalPts={score}
correctAnswers={correctAnswersCount}
totalQuestions={questions.length}
timeTaken={formatTimeTaken()}
onClaim={() => alert("Points Claimed!")}
/>
<div>
<QuizCompletionStats />
<LevelComplete
totalPts={score}
correctAnswers={correctAnswersCount}
totalQuestions={questions.length}
timeTaken={formatTimeTaken()}
onClaim={() => alert("Points Claimed!")}
/>
</div>
) : (
<div className="w-full space-y-12">
<h2 className="text-[28px] mt-10 font-semibold text-center">
Expand Down
33 changes: 33 additions & 0 deletions frontend/components/quiz/QuizCompletionStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useCompletion } from '@/features/completion';
import { useEffect } from 'react';

export function QuizCompletionStats() {
const { stats, isClaiming, isSuccess, error, claimPoints, refreshStats } = useCompletion();

useEffect(() => {
refreshStats();
}, [refreshStats]);

if (!stats) return <div>Loading stats...</div>;

return (
<div className="p-4 bg-slate-800 rounded-lg max-w-md mx-auto mt-6">
<h2 className="text-xl font-bold mb-2">Level Completion Stats</h2>
<ul className="mb-4">
<li>Points: <b>{stats.points}</b></li>
<li>Correct: <b>{stats.correct}</b> / {stats.total}</li>
<li>Time: <b>{stats.timeSeconds}</b> seconds</li>
<li>Level: <b>{stats.level}</b></li>
</ul>
<button
className="px-4 py-2 bg-blue-600 rounded text-white disabled:opacity-50"
onClick={claimPoints}
disabled={isClaiming || isSuccess}
>
{isClaiming ? 'Claiming...' : isSuccess ? 'Points Claimed!' : 'Claim Points'}
</button>
{error && <div className="text-red-400 mt-2">{error}</div>}
{isSuccess && <div className="text-green-400 mt-2">Points successfully claimed!</div>}
</div>
);
}
14 changes: 14 additions & 0 deletions frontend/features/completion/completion.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CompletionStats } from './completion.context';

// Typed API response for user stats
export async function fetchUserStats(): Promise<CompletionStats> {
const res = await fetch('/user/stats');
if (!res.ok) throw new Error('Failed to fetch user stats');
return res.json();
}

// Typed API response for claiming points
export async function claimPoints(): Promise<void> {
const res = await fetch('/puzzles/claim-points', { method: 'POST' });
if (!res.ok) throw new Error('Failed to claim points');
}
67 changes: 67 additions & 0 deletions frontend/features/completion/completion.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";

import { createContext, useContext, useState, useCallback } from 'react';
import { fetchUserStats, claimPoints as apiClaimPoints } from './completion.api';

export interface CompletionStats {
points: number;
correct: number;
total: number;
timeSeconds: number;
level: number;
}

interface CompletionContextType {
stats: CompletionStats | null;
isClaiming: boolean;
isSuccess: boolean;
error: string | null;
claimPoints: () => Promise<void>;
refreshStats: () => Promise<void>;
}

const CompletionContext = createContext<CompletionContextType | undefined>(undefined);

export const CompletionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [stats, setStats] = useState<CompletionStats | null>(null);
const [isClaiming, setIsClaiming] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);

const refreshStats = useCallback(async () => {
setError(null);
try {
const data = await fetchUserStats();
setStats(data);
} catch (err: any) {
setError(err.message || 'Failed to fetch stats');
}
}, []);

const claimPoints = useCallback(async () => {
setIsClaiming(true);
setIsSuccess(false);
setError(null);
try {
await apiClaimPoints();
setIsSuccess(true);
await refreshStats();
} catch (err: any) {
setError(err.message || 'Failed to claim points');
} finally {
setIsClaiming(false);
}
}, [refreshStats]);

return (
<CompletionContext.Provider value={{ stats, isClaiming, isSuccess, error, claimPoints, refreshStats }}>
{children}
</CompletionContext.Provider>
);
};

export function useCompletion() {
const context = useContext(CompletionContext);
if (!context) throw new Error('useCompletion must be used within CompletionProvider');
return context;
}
1 change: 1 addition & 0 deletions frontend/features/completion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './completion.context';
5 changes: 5 additions & 0 deletions frontend/providers/CompletionFeatureProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CompletionProvider } from '../features/completion';

export default function CompletionFeatureProvider({ children }: { children: React.ReactNode }) {
return <CompletionProvider>{children}</CompletionProvider>;
}
Loading