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.

229 lines • 9.84 kB
import { QuizService } from '../services/QuizService'; import { AppStore } from '../store/AppStore'; import { DOMUtils, QuizUtils } from '../utils/index'; import { Component } from './Component'; import { QuizStartModal } from './QuizStartModal'; export class QuizList extends Component { constructor() { super('#quiz-list'); this.unsubscribe = null; this.store = AppStore.getInstance(); this.quizService = QuizService.getInstance(); this.searchInput = document.querySelector('#search-input'); this.filterTabs = document.querySelectorAll('.filter-tab'); } onMount() { this.unsubscribe = this.store.subscribe(state => this.onStateChange(state)); } onUnmount() { this.unsubscribe?.(); } bindEvents() { this.searchInput?.addEventListener('input', this.handleSearch.bind(this)); this.filterTabs.forEach(tab => { tab.addEventListener('click', () => { const filter = tab.dataset.filter || 'all'; this.store.setFilter(filter); }); }); const refreshBtn = document.getElementById('refresh-quizzes-btn'); refreshBtn?.addEventListener('click', this.handleRefresh.bind(this)); } handleSearch(event) { const target = event.target; this.store.setSearchTerm(target.value); } async handleRefresh() { try { const refreshBtn = document.getElementById('refresh-quizzes-btn'); const refreshIcon = refreshBtn?.querySelector('[data-lucide="refresh-cw"]'); refreshIcon?.classList.add('animate-spin'); refreshBtn?.setAttribute('disabled', 'true'); await this.store.loadQuizzes(); DOMUtils.showToast('Quiz list refreshed', 'success'); } catch (error) { console.error('Error refreshing quizzes:', error); DOMUtils.showToast('Failed to refresh quiz list', 'error'); } finally { const refreshBtn = document.getElementById('refresh-quizzes-btn'); const refreshIcon = refreshBtn?.querySelector('[data-lucide="refresh-cw"]'); refreshIcon?.classList.remove('animate-spin'); refreshBtn?.removeAttribute('disabled'); } } onStateChange(state) { this.render(); this.updateFilterTabs(state.currentFilter); } render() { const state = this.store.getState(); if (state.ui.loading) { this.renderLoading(); return; } if (state.filteredQuizzes.length === 0) { this.renderEmpty(); return; } this.renderQuizzes(state.filteredQuizzes, state.currentQuiz); } renderLoading() { this.updateElement({ innerHTML: ` <div class="text-center text-surface-500 dark:text-surface-400 py-8"> <div class="animate-spin w-8 h-8 mx-auto mb-2 border-2 border-blue-500 border-t-transparent rounded-full"></div> <p>Loading quizzes...</p> </div> `, }); } renderEmpty() { this.updateElement({ innerHTML: ` <div class="text-center text-surface-500 dark:text-surface-400 py-8"> <i data-lucide="search-x" class="w-8 h-8 mx-auto mb-2"></i> <p>No quizzes found</p> </div> `, }); this.reinitializeIcons(); } renderQuizzes(quizzes, currentQuiz) { const quizCards = quizzes.map(quiz => this.createQuizCard(quiz, currentQuiz?.id === quiz.id)); this.element.innerHTML = ''; quizCards.forEach(card => this.element.appendChild(card)); this.reinitializeIcons(); } createQuizCard(quiz, isActive) { const questionCount = quiz.questions?.length || 0; const estimatedTime = QuizUtils.calculateEstimatedTime(questionCount); const isFavorite = this.store.isFavorite(quiz.id); const isCompleted = this.store.isQuizCompleted(quiz.id); const progress = this.store.getQuizProgress(quiz.id); let cardClassName = 'quiz-card p-4 rounded-lg cursor-pointer transition-all duration-200 border '; if (isActive) { cardClassName += 'bg-blue-100 border-blue-400 dark:bg-blue-900/20 dark:border-blue-500'; } else if (isCompleted) { cardClassName += 'bg-green-50 border-green-200 hover:bg-green-100 dark:bg-green-900/10 dark:border-green-700 dark:hover:bg-green-900/20'; } else if (progress?.status === 'in-progress') { cardClassName += 'bg-yellow-50 border-yellow-200 hover:bg-yellow-100 dark:bg-yellow-900/10 dark:border-yellow-700 dark:hover:bg-yellow-900/20'; } else { cardClassName += 'bg-gray-50 border-gray-200 hover:bg-gray-100 dark:bg-surface-800 dark:border-surface-600 dark:hover:bg-surface-700'; } const card = DOMUtils.createElement('div', { className: cardClassName, dataset: { quizId: quiz.id }, }); card.innerHTML = ` <div class="flex items-start justify-between mb-2"> <h3 class="font-medium text-surface-900 dark:text-surface-100 text-sm leading-tight flex-1 mr-2"> ${DOMUtils.escapeHtml(quiz.title)} </h3> <div class="flex items-center space-x-2"> ${this.getStatusIndicator(isCompleted, progress)} <button class="favorite-btn p-1 rounded-full hover:bg-surface-200 dark:hover:bg-surface-600 transition-colors" data-quiz-id="${quiz.id}"> <i data-lucide="${isFavorite ? 'star' : 'star'}" class="w-3 h-3 ${isFavorite ? 'text-yellow-500 fill-current' : 'text-surface-400'}"></i> </button> <span class="text-xs text-surface-500 dark:text-surface-400 whitespace-nowrap"> ${questionCount}Q </span> </div> </div> ${quiz.description ? ` <p class="text-xs text-surface-600 dark:text-surface-400 mb-2 line-clamp-2"> ${DOMUtils.escapeHtml(quiz.description)} </p> ` : ''} <div class="flex items-center justify-between text-xs text-surface-500 dark:text-surface-400"> <span class="flex items-center"> <i data-lucide="clock" class="w-3 h-3 mr-1"></i> ${estimatedTime}m </span> ${quiz.category ? ` <span class="bg-surface-200 dark:bg-surface-600 px-2 py-1 rounded-full text-xs text-surface-700 dark:text-surface-300"> ${DOMUtils.escapeHtml(quiz.category)} </span> ` : ''} </div> `; card.addEventListener('click', e => { if (e.target.closest('.favorite-btn')) { return; } this.showQuizStartModal(quiz); }); const favoriteBtn = card.querySelector('.favorite-btn'); favoriteBtn?.addEventListener('click', e => { e.stopPropagation(); this.store.toggleFavorite(quiz.id); favoriteBtn.classList.add('favorited'); setTimeout(() => { favoriteBtn.classList.remove('favorited'); }, 400); this.render(); }); return card; } updateFilterTabs(currentFilter) { this.filterTabs.forEach(tab => { const isActive = tab.dataset.filter === currentFilter; tab.classList.toggle('active', isActive); tab.classList.toggle('bg-blue-100', isActive); tab.classList.toggle('text-blue-700', isActive); tab.classList.toggle('text-gray-600', !isActive); }); } async showQuizStartModal(quiz) { try { const fullQuiz = await this.quizService.getQuizById(quiz.id); QuizStartModal.show({ quiz: fullQuiz, onStart: (selectedQuiz) => { this.store.startQuiz(selectedQuiz); }, onCancel: () => { }, }); } catch (error) { console.error('Failed to load quiz details:', error); DOMUtils.showToast('Failed to load quiz details', 'error'); } } getStatusIndicator(isCompleted, progress) { if (isCompleted) { const score = progress?.score || 0; return `<div class="flex items-center text-green-600 dark:text-green-400" title="Completed with ${score}% score"> <i data-lucide="check-circle" class="w-3 h-3"></i> </div>`; } if (progress?.status === 'in-progress') { const currentQ = progress.currentQuestion || 0; const total = progress.answers ? Object.keys(progress.answers).length : 0; return `<div class="flex items-center text-yellow-600 dark:text-yellow-400" title="In progress: ${total} questions answered"> <i data-lucide="play-circle" class="w-3 h-3"></i> </div>`; } return `<div class="flex items-center text-surface-400 dark:text-surface-500" title="Available to start"> <i data-lucide="circle" class="w-3 h-3"></i> </div>`; } reinitializeIcons() { if (window.lucide?.createIcons) { window.lucide.createIcons(); } } } //# sourceMappingURL=QuizList.js.map