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
JavaScript
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