UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

358 lines 12.4 kB
import { AuthService } from '../services/AuthService'; import { QuizService } from '../services/QuizService'; import { SettingsManager } from '../services/SettingsManager'; export class AppStore { constructor() { this.listeners = new Set(); this.unsubscribeAuth = null; this.quizService = QuizService.getInstance(); this.authService = AuthService.getInstance(); this.state = this.getInitialState(); this.loadCompletedQuizzes(); this.setupAuthSubscription(); } static getInstance() { if (!AppStore.instance) { AppStore.instance = new AppStore(); } return AppStore.instance; } getInitialState() { const savedViewMode = localStorage.getItem('quiz-view-mode'); const viewMode = savedViewMode === 'single' || savedViewMode === 'list' ? savedViewMode : 'list'; const settingsManager = SettingsManager.getInstance(); const settings = settingsManager.getSettings(); const authState = this.authService.getAuthState(); return { quizzes: [], currentQuiz: null, filteredQuizzes: [], userAnswers: {}, currentFilter: 'all', searchTerm: '', theme: { mode: 'system', color: 'blue' }, settings, auth: { isAuthenticated: authState.isAuthenticated, user: authState.user, token: authState.token, isLoading: authState.isLoading, error: authState.error, showLoginModal: false, }, ui: { loading: false, showWelcome: true, showResults: false, showCelebration: false, viewMode, submitBarVisible: false, }, quiz: { completedQuizzes: new Set(), quizProgress: {}, isCompleted: false, currentQuestionIndex: 0, }, lastResult: null, }; } getState() { return { ...this.state }; } subscribe(callback) { this.listeners.add(callback); return () => { this.listeners.delete(callback); }; } setState(updates) { this.state = { ...this.state, ...updates }; this.notifyListeners(); } notifyListeners() { this.listeners.forEach(callback => callback(this.getState())); } async loadQuizzes() { this.setState({ ui: { ...this.state.ui, loading: true } }); try { const quizzes = await this.quizService.getAllQuizzes(); this.setState({ quizzes, filteredQuizzes: quizzes, ui: { ...this.state.ui, loading: false, showWelcome: quizzes.length === 0, }, }); } catch (error) { this.setState({ ui: { ...this.state.ui, loading: false } }); throw error; } } selectQuiz(quizOrId) { let quiz; if (typeof quizOrId === 'string') { const foundQuiz = this.state.quizzes.find(q => q.id === quizOrId); if (!foundQuiz) { throw new Error(`Quiz with ID ${quizOrId} not found`); } quiz = foundQuiz; } else { quiz = quizOrId; } this.setState({ currentQuiz: quiz, userAnswers: {}, ui: { ...this.state.ui, showWelcome: false }, }); } updateAnswer(questionId, answer) { this.setState({ userAnswers: { ...this.state.userAnswers, [questionId]: answer, }, }); } setFilter(filter) { this.setState({ currentFilter: filter }); this.applyFilters(); } setSearchTerm(term) { this.setState({ searchTerm: term }); this.applyFilters(); } setViewMode(mode) { this.setState({ ui: { ...this.state.ui, viewMode: mode }, }); localStorage.setItem('quiz-view-mode', mode); } applyFilters() { let filtered = [...this.state.quizzes]; if (this.state.searchTerm) { const term = this.state.searchTerm.toLowerCase(); filtered = filtered.filter(quiz => quiz.title.toLowerCase().includes(term) || (quiz.description && quiz.description.toLowerCase().includes(term)) || (quiz.category && quiz.category.toLowerCase().includes(term))); } switch (this.state.currentFilter) { case 'recent': filtered.sort((a, b) => new Date(b.updatedAt || b.createdAt || '').getTime() - new Date(a.updatedAt || a.createdAt || '').getTime()); break; case 'favorites': filtered = filtered.filter(quiz => localStorage.getItem(`favorite-${quiz.id}`) === 'true'); break; } this.setState({ filteredQuizzes: filtered }); } showResults() { this.setState({ ui: { ...this.state.ui, showResults: true }, }); } hideResults() { this.setState({ ui: { ...this.state.ui, showResults: false }, }); } retakeQuiz() { this.setState({ userAnswers: {}, ui: { ...this.state.ui, showResults: false }, }); } isFavorite(quizId) { return localStorage.getItem(`favorite-${quizId}`) === 'true'; } toggleFavorite(quizId) { const currentValue = this.isFavorite(quizId); localStorage.setItem(`favorite-${quizId}`, (!currentValue).toString()); if (this.state.currentFilter === 'favorites') { this.applyFilters(); } } async submitQuiz() { if (!this.state.currentQuiz) { throw new Error('No quiz selected'); } const totalQuestions = this.state.currentQuiz.questions.length; const answeredQuestions = Object.keys(this.state.userAnswers).length; if (answeredQuestions < totalQuestions) { console.warn(`⚠️ Submitting incomplete quiz: ${answeredQuestions}/${totalQuestions} answered`); if (process.env.NODE_ENV === 'production' && answeredQuestions === 0) { throw new Error('Please answer at least one question before submitting'); } } try { const result = await this.quizService.submitQuiz(this.state.currentQuiz.id, this.state.userAnswers); this.state.lastResult = result; this.markQuizCompleted(this.state.currentQuiz.id, result); this.showResults(); return result; } catch (error) { throw error; } } loadCompletedQuizzes() { try { const stored = localStorage.getItem('completed-quizzes'); if (stored) { const completedIds = JSON.parse(stored); this.state.quiz.completedQuizzes = new Set(completedIds); } const progressStored = localStorage.getItem('quiz-progress'); if (progressStored) { this.state.quiz.quizProgress = JSON.parse(progressStored); } } catch (error) { console.warn('Failed to load completed quiz data:', error); } } saveCompletedQuizzes() { try { const completedIds = Array.from(this.state.quiz.completedQuizzes); localStorage.setItem('completed-quizzes', JSON.stringify(completedIds)); localStorage.setItem('quiz-progress', JSON.stringify(this.state.quiz.quizProgress)); } catch (error) { console.error('Failed to save completed quiz data:', error); } } markQuizCompleted(quizId, result) { this.state.quiz.completedQuizzes.add(quizId); const progress = { quizId, status: 'completed', answers: this.state.userAnswers, completedAt: new Date(), score: result.score, timeSpent: result.timeSpent, }; this.state.quiz.quizProgress[quizId] = progress; this.state.quiz.isCompleted = true; this.saveCompletedQuizzes(); this.notifyListeners(); console.log(`✅ Quiz completed: ${quizId}`, progress); } isQuizCompleted(quizId) { return this.state.quiz.completedQuizzes.has(quizId); } getQuizProgress(quizId) { return this.state.quiz.quizProgress[quizId]; } getCompletedQuizIds() { return Array.from(this.state.quiz.completedQuizzes); } resetQuizCompletion() { this.state.quiz.isCompleted = false; this.state.quiz.currentQuestionIndex = 0; this.notifyListeners(); } startQuiz(quiz) { if (!quiz.questions || !Array.isArray(quiz.questions) || quiz.questions.length === 0) { console.error('Cannot start quiz: missing or invalid questions', quiz); throw new Error('Quiz has no questions available'); } this.selectQuiz(quiz); this.resetQuizCompletion(); const progress = { quizId: quiz.id, status: 'in-progress', answers: {}, startedAt: new Date(), }; this.state.quiz.quizProgress[quiz.id] = progress; this.saveCompletedQuizzes(); try { import('../components/SidebarToggle').then(({ SidebarToggle }) => { const sidebarToggle = SidebarToggle.getInstance(); if (sidebarToggle) { sidebarToggle.autoHideOnMobileForQuiz(); } }); } catch (error) { console.warn('Could not auto-hide sidebar on mobile:', error); } console.log(`🚀 Started quiz: ${quiz.id}`); } showCelebration() { this.state.ui.showCelebration = true; this.notifyListeners(); setTimeout(() => { this.state.ui.showCelebration = false; this.notifyListeners(); }, 3000); } updateSubmitBarVisibility() { const shouldShow = this.state.currentQuiz && !this.state.ui.showWelcome && !this.state.ui.showResults && !this.state.quiz.isCompleted; this.state.ui.submitBarVisible = shouldShow || false; this.notifyListeners(); } setCurrentQuestionIndex(index) { this.state.quiz.currentQuestionIndex = index; if (this.state.currentQuiz) { const progress = this.state.quiz.quizProgress[this.state.currentQuiz.id]; if (progress) { progress.currentQuestion = index; progress.answers = { ...this.state.userAnswers }; this.saveCompletedQuizzes(); } } this.notifyListeners(); } setupAuthSubscription() { this.unsubscribeAuth = this.authService.subscribe(authState => { this.setState({ auth: { ...this.state.auth, isAuthenticated: authState.isAuthenticated, user: authState.user, token: authState.token, isLoading: authState.isLoading, error: authState.error, }, }); }); } showLoginModal() { this.setState({ auth: { ...this.state.auth, showLoginModal: true }, }); } hideLoginModal() { this.setState({ auth: { ...this.state.auth, showLoginModal: false }, }); } async login(credentials) { return this.authService.login(credentials); } async logout() { await this.authService.logout(); } getCurrentUser() { return this.state.auth.user; } isAuthenticated() { return this.state.auth.isAuthenticated; } async authenticatedFetch(url, options = {}) { return this.authService.authenticatedFetch(url, options); } destroy() { this.unsubscribeAuth?.(); } } //# sourceMappingURL=AppStore.js.map