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
11 changes: 9 additions & 2 deletions frontend/src/app/games/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function GamesPageContent() {
const { students: allStudents } = useStudents()
const [selectedContent, setSelectedContent] = React.useState<{
theme: Theme;
themeWeek: number;
difficultyLevel: number;
category: GetGameContentsCategory;
questionType: GetGameContentsQuestionType;
Expand Down Expand Up @@ -107,6 +108,7 @@ function GamesPageContent() {

const handleContentSelection = (selection: {
theme: Theme;
themeWeek: number;
difficultyLevel: number;
category: GetGameContentsCategory;
questionType: GetGameContentsQuestionType;
Expand Down Expand Up @@ -232,6 +234,7 @@ const handleSubmitResult = async () => {
onClick={() => {
const params = new URLSearchParams({
themeId: selectedContent.theme.id,
themeWeek: selectedContent.themeWeek !== null ? String(selectedContent.themeWeek) : '',
difficulty: String(selectedContent.difficultyLevel),
category: selectedContent.category,
questionType: selectedContent.questionType,
Expand All @@ -249,6 +252,7 @@ const handleSubmitResult = async () => {
onClick={() => {
const params = new URLSearchParams({
themeId: selectedContent.theme.id,
themeWeek: selectedContent.themeWeek !== null ? String(selectedContent.themeWeek) : '',
difficulty: String(selectedContent.difficultyLevel),
category: selectedContent.category,
questionType: selectedContent.questionType,
Expand All @@ -266,23 +270,25 @@ const handleSubmitResult = async () => {
onClick={() => {
const params = new URLSearchParams({
themeId: selectedContent.theme.id,
themeWeek: selectedContent.themeWeek !== null ? String(selectedContent.themeWeek) : '',
difficulty: String(selectedContent.difficultyLevel),
category: selectedContent.category,
questionType: selectedContent.questionType,
sessionId,
});
router.push(`/games/memorymatch?${params.toString()}`);
router.push(`/games/spinner?${params.toString()}`);
}}
className="bg-pink cursor-pointer hover:bg-pink-hover text-white p-6 rounded-lg font-semibold transition-all hover:scale-105 text-left flex items-center gap-4"
>
<Brain className="w-6 h-6 shrink-0" />
<span>Memory Match</span>
<span>Spinner</span>
</button>

<button
onClick={() => {
const params = new URLSearchParams({
themeId: selectedContent.theme.id,
themeWeek: selectedContent.themeWeek !== null ? String(selectedContent.themeWeek) : '',
difficulty: String(selectedContent.difficultyLevel),
category: selectedContent.category,
questionType: selectedContent.questionType,
Expand All @@ -300,6 +306,7 @@ const handleSubmitResult = async () => {
onClick={() => {
const params = new URLSearchParams({
themeId: selectedContent.theme.id,
themeWeek: selectedContent.themeWeek !== null ? String(selectedContent.themeWeek) : '',
difficulty: String(selectedContent.difficultyLevel),
category: selectedContent.category,
questionType: selectedContent.questionType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"use client";

import MemorymatchGameInterface from "@/components/games/MemorymatchInterface";
import SpinnerGameInterface from "@/components/games/SpinnerInterface";
import { StudentSelector } from "@/components/games/StudentSelector";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";

export function MemorymatchContent() {
export function SpinnerContent() {
const router = useRouter();
const searchParams = useSearchParams();
const sessionId = searchParams.get("sessionId") || "00000000-0000-0000-0000-000000000000";
const sessionStudentIdsParam = searchParams.get("sessionStudentIds");
const themeId = searchParams.get("themeId");
const themeWeek = searchParams.get("themeWeek");
const themeName = searchParams.get("themeName");
const difficulty = searchParams.get("difficulty");
const category = searchParams.get("category");
Expand Down Expand Up @@ -42,24 +43,25 @@ export function MemorymatchContent() {
if (selectedStudentIds.length === 0) {
return (
<StudentSelector
gameTitle="Memory Match"
gameTitle="Spinner"
onBack={() => router.back()}
onStudentsSelected={(studentIds) => {
setSelectedStudentIds(studentIds);
// Update URL with selected students
const params = new URLSearchParams(searchParams.toString());
params.set('sessionStudentIds', studentIds.join(','));
router.replace(`/games/memorymatch?${params.toString()}`);
router.replace(`/games/spinner?${params.toString()}`);
}}
/>
);
}

return (
<MemorymatchGameInterface
<SpinnerGameInterface
session_student_ids={selectedStudentIds.map(id => Number.parseInt(id))}
session_id={sessionId}
themeId={themeId}
themeWeek={themeWeek ? Number.parseInt(themeWeek) : 1}
themeName={themeName || "Theme"}
difficulty={Number.parseInt(difficulty)}
category={category}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { MemorymatchContent } from "./MemorymatchContent";
import { SpinnerContent } from "./SpinnerContent";

export const metadata: Metadata = {
title: "Memory Match Game",
title: "Spinner Game",
description: "Practice with interactive wheel",
};

Expand All @@ -18,10 +18,10 @@ function LoadingSpinner() {
);
}

export default function MemorymatchPage() {
export default function SpinnerPage() {
return (
<Suspense fallback={<LoadingSpinner />}>
<MemorymatchContent />
<SpinnerContent />
</Suspense>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default function WordImageMatchingContent() {

const themeId = searchParams.get('themeId')
const themeName = searchParams.get('themeName')
const themeWeek = searchParams.get('themeWeek')
const difficulty = searchParams.get('difficulty')
const category = searchParams.get('category')
const questionType = searchParams.get('questionType')
Expand Down Expand Up @@ -63,6 +64,7 @@ export default function WordImageMatchingContent() {
session_student_ids={selectedStudentIds.map(id => Number.parseInt(id))}
session_id={sessionId}
themeID={themeId}
themeWeek={themeWeek ? Number.parseInt(themeWeek) : 1}
themeName={themeName}
difficulty={difficulty}
category={category}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/games/GameContentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const STARS = [
interface GameContentSelectorProps {
onSelectionComplete: (selection: {
theme: Theme
themeWeek: number
difficultyLevel: number
category: GetGameContentsCategory
questionType: GetGameContentsQuestionType
Expand Down Expand Up @@ -112,6 +113,7 @@ export function GameContentSelector({
if (theme && selectedCategory && selectedQuestionType) {
onSelectionComplete({
theme,
themeWeek: currentWeek || 1,
difficultyLevel,
category: selectedCategory,
questionType: selectedQuestionType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ const CATEGORIES = {
},
};

interface MemorymatchGameInterfaceProps {
interface SpinnerGameInterfaceProps {
session_student_ids: number[];
session_id?: string;
themeId: string;
themeWeek: number;
themeName: string;
difficulty: number;
category: string;
Expand Down Expand Up @@ -203,15 +204,16 @@ const SpinnerWheel: React.FC<{
);
};

export default function MemorymatchGameInterface({
export default function SpinnerGameInterface({
session_student_ids,
session_id,
themeId,
themeWeek,
themeName,
difficulty,
category,
questionType,
}: MemorymatchGameInterfaceProps) {
}: SpinnerGameInterfaceProps) {
const router = useRouter();
const [cardStartTime, setCardStartTime] = useState<number | null>(null);
const [timeTaken, setTimeTaken] = useState(0);
Expand All @@ -234,6 +236,7 @@ export default function MemorymatchGameInterface({
error: contentsError,
} = useGameContents({
theme_id: themeId,
theme_week: themeWeek ?? undefined,
category: category as GetGameContentsCategory,
question_type: questionType as GetGameContentsQuestionType,
difficulty_level: difficulty,
Expand Down Expand Up @@ -425,7 +428,7 @@ export default function MemorymatchGameInterface({
<div className="text-center">
<p className="text-error mb-4">
{contentsError
? "Failed to load Memory Match"
? "Failed to load Spinner"
: "No words available"}
</p>
<button
Expand Down Expand Up @@ -460,7 +463,7 @@ export default function MemorymatchGameInterface({
</button>
</div>

<h1 className="mb-4">Memory Match</h1>
<h1 className="mb-4">Spinner</h1>

{/* Current Student Banner */}
<div className="bg-blue text-white rounded-lg p-4 mb-6 text-center">
Expand Down
22 changes: 17 additions & 5 deletions frontend/src/components/games/WordImageMatchingGameInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface WordImageMatchingGameInterfaceProps {
session_id: string
themeID: string
themeName: string | null
themeWeek: number
difficulty: string
category: string
questionType: string
Expand All @@ -41,6 +42,7 @@ export default function WordImageMatchingGameInterface({
session_id,
themeID,
themeName,
themeWeek,
difficulty,
category,
questionType
Expand All @@ -65,8 +67,10 @@ export default function WordImageMatchingGameInterface({

const currentSessionStudentId = session_student_ids[currentStudentIndex]

// FIX #1: Properly handle theme_week filtering
const { gameContents, isLoading } = useGameContents({
theme_id: themeID || undefined,
theme_week: themeWeek !== null ? themeWeek : undefined, // Only pass if not null
difficulty_level: difficulty ? Number.parseInt(difficulty) : undefined,
category: category as any,
question_type: questionType as any,
Expand Down Expand Up @@ -96,7 +100,7 @@ export default function WordImageMatchingGameInterface({
{ key: 'words', cards: wordCards },
]

// Initialize when student changes or game contents load
// FIX #3: Add gameContents to dependency array to handle late loading
useEffect(() => {
if (gameContents.length === 0) return
if (completedStudents.has(currentSessionStudentId)) return
Expand Down Expand Up @@ -131,7 +135,7 @@ export default function WordImageMatchingGameInterface({
})
setCardStartTimes(startTimes)
setIncorrectAttempts(new Map())
}, [currentStudentIndex]) // Only re-run when student changes
}, [currentStudentIndex, gameContents, completedStudents, currentSessionStudentId]) // Added dependencies

// Reset game function for "Play Again" button
const resetGame = () => {
Expand Down Expand Up @@ -394,6 +398,13 @@ export default function WordImageMatchingGameInterface({
<div className="flex flex-wrap items-center gap-2 text-sm">
<span className="text-muted">Theme: </span>
<span className="font-medium text-primary">{themeName}</span>
{themeWeek !== null && (
<>
<span className="text-muted mx-2">•</span>
<span className="text-muted">Week:</span>
<span className="font-medium text-primary">{themeWeek}</span>
</>
)}
<span className="text-muted mx-2">•</span>
<span className="text-muted">Difficulty:</span>
<span className="font-medium text-primary">Level {difficulty}</span>
Expand All @@ -403,9 +414,10 @@ export default function WordImageMatchingGameInterface({
</div>
</div>

<div className="grid grid-cols-2 gap-8">
{/* FIX #2: Improved card grid with better spacing */}
<div className="grid grid-cols-2 gap-6">
{groupedCols.map(group => (
<div key={group.key} className="flex flex-col gap-4">
<div key={group.key} className="flex flex-col gap-3">
{group.cards.map(card => (
<MatchingCard
key={card.id}
Expand All @@ -423,4 +435,4 @@ export default function WordImageMatchingGameInterface({
</div>
</div>
)
}
}
42 changes: 29 additions & 13 deletions frontend/src/components/games/word-image-match/MatchingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use client'

import React from 'react'
import clsx from 'clsx'

Expand All @@ -21,21 +20,38 @@ export default function MatchingCard({
isMatched = false,
onClick,
disabled = false,
}: MatchingCardProps) {
}: MatchingCardProps) {
return (
<button onClick={onClick}
disabled={disabled || isMatched}
className={clsx("relative flex items-center justify-center rounded-lg border p-4 bg-card transition-all",
"hover:shadow-md active:scale-[0.97]",
isMatched && "border-green-500 border-2 bg-green/15 cursor-not-allowed",
isWrong && "border-red-500 border-2 shadow-red-500/40",
isSelected && !isWrong && !isMatched && "border-blue shadow-lg bg-card-hover"
)}>
<button
onClick={onClick}
disabled={disabled || isMatched}
className={clsx(
"relative flex items-center justify-center rounded-lg transition-all",
"hover:shadow-md active:scale-[0.97]",
"w-full h-[200px]",

isImage ? "p-6" : "p-6",

// BACKGROUND STATES (no borders)
isMatched && "bg-green-200 cursor-not-allowed",
isWrong && "bg-red-200",
isSelected && !isWrong && !isMatched && "bg-blue-200",

// Default background
!isMatched && !isWrong && !isSelected && "bg-card"
)}
>
{!isImage ? (
<span className="w-50 h-30 text-5xl font-semibold text-foreground px-2 pt-[7.5%]">{value}</span>
<span className="text-3xl font-semibold text-foreground text-center leading-relaxed break-words max-w-full px-2">
{value}
</span>
) : (
<img src={value} alt="Image" className="w-50 h-30 object-contain p-2"/>
<img
src={value}
alt="Matching card image"
className="w-full h-full object-contain"
/>
)}
</button>
)
}
}