export type QuestionStatus = 'not_started' | 'in_progress' | 'completed' | 'skipped'; export interface QuizQuestion { id: number; title: string; is_skippable: boolean; type: 'single' | 'multiple' | 'text'; options?: { id: string; label: string }[]; } export interface AnswerState { questionId: number; value: string | string[] | null; is_saved: boolean; status: QuestionStatus; touched: boolean; last_saved_at?: string; } // Mock Data const MOCK_QUESTIONS: QuizQuestion[] = [ { id: 1, title: 'What is the capital of France?', is_skippable: true, type: 'single', options: [ { id: 'london', label: 'London' }, { id: 'paris', label: 'Paris' }, { id: 'berlin', label: 'Berlin' }, ], }, { id: 2, title: 'Explain the concept of closure in JavaScript.', is_skippable: false, type: 'text', }, { id: 3, title: 'Which of the following are Vue lifecycle hooks? (Select all that apply)', is_skippable: true, type: 'multiple', options: [ { id: 'created', label: 'created' }, { id: 'mounted', label: 'mounted' }, { id: 'render', label: 'render' }, { id: 'compute', label: 'compute' }, ], }, { id: 4, title: 'What is 2 + 2?', is_skippable: false, type: 'single', options: [ { id: '3', label: '3' }, { id: '4', label: '4' }, { id: '5', label: '5' }, ], } ]; export const useQuizRunner = () => { // State (using useState for Nuxt SSR safety and persistence across component reloads if needed) const questions = useState('quiz-questions', () => []); const answers = useState>('quiz-answers', () => ({})); const currentQuestionIndex = useState('quiz-current-index', () => 0); const loading = useState('quiz-loading', () => false); const lastError = useState('quiz-error', () => null); // Getters const currentQuestion = computed(() => questions.value[currentQuestionIndex.value]); const currentAnswer = computed(() => { if (!currentQuestion.value) return null; return answers.value[currentQuestion.value.id]; }); const totalQuestions = computed(() => questions.value.length); const isLastQuestion = computed(() => currentQuestionIndex.value === questions.value.length - 1); const isFirstQuestion = computed(() => currentQuestionIndex.value === 0); // Actions function initQuiz(quizId: string) { questions.value = [...MOCK_QUESTIONS]; currentQuestionIndex.value = 0; answers.value = {}; lastError.value = null; questions.value.forEach(q => { answers.value[q.id] = { questionId: q.id, value: q.type === 'multiple' ? [] : null, is_saved: false, status: 'not_started', touched: false, }; }); if (questions.value.length > 0) { enterQuestion(questions.value[0].id); } } function enterQuestion(qId: number) { const ans = answers.value[qId]; if (ans) { ans.touched = true; // Mark as in_progress if not final state if (ans.status === 'not_started' || ans.status === 'skipped') { ans.status = 'in_progress'; } } } function canLeaveCurrent(): { allowed: boolean; reason?: string } { if (!currentQuestion.value) return { allowed: true }; const q = currentQuestion.value; const a = answers.value[q.id]; if (a.status === 'completed' || a.is_saved) return { allowed: true }; if (q.is_skippable) return { allowed: true }; // Required and unsaved if (!a.is_saved) { return { allowed: false, reason: 'This question is required and must be saved.' }; } return { allowed: true }; } function updateAnswer(val: string | string[] | null) { if (!currentQuestion.value) return; const qId = currentQuestion.value.id; answers.value[qId].value = val; // If modifying a completed answer, revert to in_progress until saved again? // Yes, to enforce "Green = Saved Successfully" implies current state matches saved state. if (answers.value[qId].is_saved) { answers.value[qId].is_saved = false; answers.value[qId].status = 'in_progress'; } } async function saveCurrentAnswer() { if (!currentQuestion.value) return; const qId = currentQuestion.value.id; const ans = answers.value[qId]; // Validation if (currentQuestion.value.type === 'multiple') { if (!ans.value || (ans.value as string[]).length === 0) { lastError.value = "Please select at least one option."; return false; } } else { if (!ans.value || (ans.value as string).trim() === '') { lastError.value = "Please provide an answer."; return false; } } loading.value = true; lastError.value = null; try { await new Promise(resolve => setTimeout(resolve, 800)); ans.is_saved = true; ans.status = 'completed'; ans.last_saved_at = new Date().toISOString(); return true; } catch (e) { lastError.value = "Failed to save answer."; return false; } finally { loading.value = false; } } function handleLeaveLogic(targetIndex: number) { if (targetIndex === currentQuestionIndex.value) return; const check = canLeaveCurrent(); if (!check.allowed) { lastError.value = check.reason || "Required question."; return false; } // Mark current as skipped if leaving without completion const currQ = currentQuestion.value; const currAns = answers.value[currQ.id]; // If we leave and it is NOT completed (and implicit skippable check passed), set SKIPPED if (currAns.status !== 'completed' && !currAns.is_saved) { currAns.status = 'skipped'; } currentQuestionIndex.value = targetIndex; lastError.value = null; if (questions.value[targetIndex]) { enterQuestion(questions.value[targetIndex].id); } return true; } async function nextQuestion() { if (isLastQuestion.value) return; handleLeaveLogic(currentQuestionIndex.value + 1); } async function prevQuestion() { if (isFirstQuestion.value) return; handleLeaveLogic(currentQuestionIndex.value - 1); } async function goToQuestion(index: number) { if (index < 0 || index >= questions.value.length) return; handleLeaveLogic(index); } async function skipQuestion() { if (isLastQuestion.value) { // If last question and skip... maybe just mark skipped? const currQ = currentQuestion.value; const currAns = answers.value[currQ.id]; currAns.status = 'skipped'; return; } handleLeaveLogic(currentQuestionIndex.value + 1); } return { questions, answers, currentQuestionIndex, loading, lastError, currentQuestion, currentAnswer, totalQuestions, isFirstQuestion, isLastQuestion, initQuiz, updateAnswer, saveCurrentAnswer, nextQuestion, prevQuestion, goToQuestion, skipQuestion }; };