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