integratedhub/islands/Quizzing.tsx
2026-01-20 09:02:26 -06:00

417 lines
13 KiB
TypeScript

// islands/Quizzing.tsx
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
//import { QuizData } from "../utils/database.ts";
import { QuizConfig } from "../utils/types.ts";
import { Signal, signal } from "@preact/signals";
import Card from "./Card.tsx";
import { stringify } from "node:querystring";
interface QuizzingIProps {
initialQuizData: QuizConfig;
runData: string;
}
export const STORAGE_KEY = "QUIZ_STATE";
const useAudio1 = signal(true);
const useAudio2 = signal(true);
const useInputHint = signal(false);
export default function Quizzing({ initialQuizData, runData }: QuizzingIProps) {
const hideText = initialQuizData &&
(initialQuizData.quiz_mode === "photo-hide-english" ||
initialQuizData.quiz_mode === "photo-hide-bisaya");
const [quizData, setQuizData] = useState<QuizConfig>(initialQuizData);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [questionText, setQuextionText] = useState(
quizData ? quizData.questions[0].text_1 : "",
);
const [meaningText, setMeaningText] = useState("");
const [backgroundUrl, setCurrentBackgroundUrl] = useState("");
const [quizCompleted, setQuizCompleted] = useState(false);
const [runFullAuto, setRunFullAuto] = useState(false);
const [isPausedDisplay, setIsPausedDisplay] = useState(false);
const togglePauseRef = useRef<() => void>(() => {});
const [countdown, setCountdown] = useState(3);
const postButtonRef = useRef<HTMLButtonElement>(null);
const [count, setCount] = useState(0);
const [cardKey, setCardKey] = useState(0);
const runFullAutoRef = useRef(runFullAuto);
const meaningTextRef = useRef(meaningText);
const quizDataRef = useRef(quizData);
useEffect(() => {
runFullAutoRef.current = runFullAuto;
}, [runFullAuto]);
useEffect(() => {
meaningTextRef.current = meaningText;
}, [meaningText]);
useEffect(() => {
quizDataRef.current = quizData;
}, [quizData]);
const [cardInputParams, seCardInputParams] = useState({
setFullAuto: setRunFullAuto,
runFullAuto: false,
setTogglePause: (fn: () => void) => {
togglePauseRef.current = fn;
},
setIsPausedDisplay: setIsPausedDisplay,
questionPhrase: quizData ? quizData.questions[0].text_1 : "",
answerPhrase: "",
nextButton: () => {},
audio1: "",
audio2: "",
onHelpUsed: () => {},
flipped: false,
});
const handleClick = () => {
setCount((prev) => {
const newCount = prev + 1;
//eventEmitter.emit("countUpdated", newCount);
return newCount;
});
};
// Ref to track current index synchronously (state updates are async)
const currentIndexRef = useRef(0);
const incTheIndex = (auto: boolean): number => {
let nextIndex = currentIndexRef.current + 1;
if (auto && nextIndex >= initialQuizData.questions.length) {
nextIndex = 0;
}
currentIndexRef.current = nextIndex;
setCurrentQuestionIndex(nextIndex);
return nextIndex;
};
useEffect(() => {
if (quizCompleted && countdown > 0) {
const timer = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
return () => clearTimeout(timer);
} else if (quizCompleted && countdown === 0) {
if (postButtonRef.current) {
//console.log(runData);
postButtonRef.current.click();
}
//wind ow.location.href = "/language/dashboard";
}
}, [quizCompleted, countdown]);
const handleNextQuestionInternal = useCallback((forceAuto?: boolean) => {
if (forceAuto === true && !runFullAutoRef.current) {
runFullAutoRef.current = true;
setRunFullAuto(true);
}
const thisAuto = typeof forceAuto === "boolean"
? forceAuto
: runFullAutoRef.current;
const quizDataNow = quizDataRef.current;
const meaningTextNow = meaningTextRef.current;
// Determine the next index synchronously
// On first call (meaningText === ""), stay at index 0
// On subsequent calls, increment
const isFirstCall = meaningTextNow === "";
const thisIndex = isFirstCall ? 0 : incTheIndex(thisAuto);
if (thisIndex < quizDataNow.questions.length) {
// Get ALL data from the SAME question object
const thisQuestion = quizDataNow.questions[thisIndex];
const text_1 = thisQuestion.text_1;
const text_2 = thisQuestion.text_2;
const audio1 = useAudio1.value ? thisQuestion.audio_file_1 : "";
const audio2 = useAudio2.value ? thisQuestion.audio_file_2 : "";
const thisDataUrl = thisQuestion.background_image_data || "";
// Update all state together
setQuextionText(text_1);
setMeaningText(text_2);
meaningTextRef.current = text_2;
setCurrentBackgroundUrl(thisDataUrl);
const cardParams = {
setFullAuto: setRunFullAuto,
runFullAuto: thisAuto,
setTogglePause: (fn: () => void) => {
togglePauseRef.current = fn;
},
setIsPausedDisplay: setIsPausedDisplay,
questionPhrase: text_1,
answerPhrase: text_2,
nextButton: handleNextQuestion,
audio1: audio1,
audio2: audio2,
onHelpUsed: () => {},
flipped: initialQuizData.quiz_mode === "photo-hide-english" ||
initialQuizData.quiz_mode === "photo-see-english" ||
initialQuizData.quiz_mode === "blank-see-english",
};
seCardInputParams(cardParams);
setCardKey((prev) => prev + 1); // Force Card remount
handleClick();
} else {
//localStorage.removeItem("QUIZ_STATE");
setQuizCompleted(true);
}
}, [initialQuizData]);
const handleNextQuestion = useCallback(() => {
handleNextQuestionInternal();
}, [handleNextQuestionInternal]);
if (!initialQuizData || !runData) {
return <div>Unable to load quiz</div>;
}
if (!quizData) {
return <div>Loading...</div>;
}
if (quizCompleted) {
return (
<div className="flex flex-col items-center justify-start pt-20 min-h-screen">
<h2 className="text-2xl font-bold mb-4 theme-text">Quiz Completed!</h2>
<div className="relative w-24 h-24">
<svg
className="animate-spin -ml-1 mr-3 h-24 w-24 text-blue-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
>
</circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
>
</path>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-2xl font-bold">
{countdown}
</span>
</div>
<p className="mt-4 theme-text">Redirecting ...</p>
<form
hidden
action="/language/set-quiz"
method="POST"
>
<input
type="hidden"
name="quizConfigData"
value={runData}
/>
<input
type="hidden"
name="quizMode"
value={initialQuizData.quiz_mode}
/>
<button hidden ref={postButtonRef} type="submit">
</button>
</form>
</div>
);
}
//const currentQuestion = quizData.questions[currentQuestionIndex];
//if (quizStarted){
return (
<div
className={`theme-container p-2 sm:p-3 md:p-4 rounded-lg shadow-md max-w-md mx-auto w-full overflow-clip`}
>
<div hidden>
<button onClick={handleClick}>Increment</button>
<p>Count: {count}</p>
</div>
{meaningText !== ""
? (
<Card
key={cardKey}
cardInputParams={cardInputParams}
backgroundUrl={backgroundUrl}
hideText={hideText}
useInputHint={useInputHint}
/>
)
: (
<div className="text-center p-0">
<p className="theme-text text-xl mb-4">Ready to start quiz</p>
<p className="theme-text text-sm">
Questions: {quizData.questions.length}
</p>
</div>
)}
{meaningText === ""
? (
<div className="mt-0 flex justify-center w-full px-2">
<div className="theme-card w-80 p-4 rounded-lg shadow-md theme-text">
<div className="text-sm opacity-90 mb-3">
Keyboard: shift + return = help
</div>
<div className="space-y-3">
<label className="flex items-center justify-between gap-3">
<span>Play recording 1</span>
<input
type="checkbox"
checked={useAudio1.value}
onChange={(
e,
) => (useAudio1.value = e.currentTarget.checked)}
/>
</label>
<label className="flex items-center justify-between gap-3">
<span>Play recording 2</span>
<input
type="checkbox"
checked={useAudio2.value}
onChange={(
e,
) => (useAudio2.value = e.currentTarget.checked)}
/>
</label>
<label className="flex items-center justify-between gap-3">
<span>Input hint on</span>
<input
type="checkbox"
checked={useInputHint.value}
onChange={(
e,
) => (useInputHint.value = e.currentTarget.checked)}
/>
</label>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
<button
onClick={() => {
setRunFullAuto(true);
handleNextQuestionInternal(true);
}}
className="bg-blue-500 text-white px-4 py-2 rounded-xl hover:bg-blue-600 w-full whitespace-nowrap"
title={"Listen Through"}
>
Listen Through
</button>
<button
onClick={handleNextQuestion}
className="bg-blue-500 text-white px-4 py-2 rounded-xl hover:bg-blue-600 w-full whitespace-nowrap"
title={"Start"}
>
Start Quiz
</button>
</div>
</div>
</div>
)
: runFullAuto
? (
<button
type="button"
id="auto-pause-play-button"
onClick={() => togglePauseRef.current()}
style={{ top: "320px", left: "50%", marginLeft: "-12px" }}
className="bg-opacity-50 fixed bg-blue-500 hover:bg-blue-500 hover:bg-opacity-50 transition duration-300 text-white font-bold p-1 rounded-full"
aria-label={isPausedDisplay ? "Resume auto" : "Pause auto"}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
{isPausedDisplay
? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
)
: (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 9v6m4-6v6"
/>
)}
</svg>
</button>
)
: (
<button
type="button"
id="next-question-button"
onClick={handleNextQuestion}
style={{ top: "305px", left: "50%", marginLeft: "-12px" }}
className="bg-opacity-50 fixed bg-blue-500 hover:bg-blue-500 hover:bg-opacity-50 transition duration-300 text-white font-bold p-1 rounded-full"
aria-label="Next question"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
)}
</div>
);
// }
// else{
// return (
// <div className=" bg-opacity-10 backdrop-filter backdrop-blur-lg rounded-xl shadow-2xl p-8 max-w-md w-full">
// <button
// //type="submit"
// onClick={() => {setQuizStarted(true)}}
// className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 w-full"
// >
// Start
// </button>
// </div>
// );
// }
// Placeholder function for fetching quiz data
// async function fetchQuizData(quiz_id: number): Promise<{}> {
// // Implement the actual data fetching logic here
// // This is just a placeholder
// return { quiz_id: quiz_id, questions: [] };
// }
}