UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

632 lines (549 loc) 19.4 kB
// MusPE MVC Architecture Example - Task Manager Application import { MusPEModel, MusPEView, MusPEController } from '../core/MVC.js'; // ============ TASK MODEL ============ class TaskModel extends MusPEModel { constructor(data = {}) { super(data, { validationRules: { title: [ { validator: 'required', message: 'Task title is required' }, { validator: 'minLength', params: [3], message: 'Title must be at least 3 characters' } ], priority: [ { validator: 'required', message: 'Priority is required' } ], dueDate: [ { validator: 'date', message: 'Please enter a valid date' } ] }, computedProperties: { isOverdue: function() { const dueDate = new Date(this.get('dueDate')); return dueDate < new Date() && !this.get('completed'); }, displayPriority: function() { const priority = this.get('priority'); const priorityMap = { 1: 'Low', 2: 'Medium', 3: 'High', 4: 'Critical' }; return priorityMap[priority] || 'Unknown'; }, progressPercentage: function() { const subtasks = this.get('subtasks') || []; if (subtasks.length === 0) return this.get('completed') ? 100 : 0; const completed = subtasks.filter(task => task.completed).length; return Math.round((completed / subtasks.length) * 100); } }, trackChanges: true, enableUndo: true }); // Set default values this.setData({ id: this.generateId(), title: '', description: '', priority: 2, completed: false, createdAt: new Date().toISOString(), dueDate: null, tags: [], subtasks: [], ...data }, false); } generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } // Business logic methods complete() { this.set('completed', true); this.set('completedAt', new Date().toISOString()); this.emit('task:completed', this.getData()); } uncomplete() { this.set('completed', false); this.set('completedAt', null); this.emit('task:uncompleted', this.getData()); } addSubtask(title) { const subtasks = [...this.get('subtasks')]; subtasks.push({ id: this.generateId(), title, completed: false, createdAt: new Date().toISOString() }); this.set('subtasks', subtasks); } updateSubtask(subtaskId, updates) { const subtasks = this.get('subtasks').map(subtask => subtask.id === subtaskId ? { ...subtask, ...updates } : subtask ); this.set('subtasks', subtasks); } removeSubtask(subtaskId) { const subtasks = this.get('subtasks').filter(subtask => subtask.id !== subtaskId); this.set('subtasks', subtasks); } addTag(tag) { const tags = [...this.get('tags')]; if (!tags.includes(tag)) { tags.push(tag); this.set('tags', tags); } } removeTag(tag) { const tags = this.get('tags').filter(t => t !== tag); this.set('tags', tags); } } // ============ TASK LIST MODEL ============ class TaskListModel extends MusPEModel { constructor() { super({ tasks: [], filter: 'all', // 'all', 'active', 'completed', 'overdue' sortBy: 'priority', // 'priority', 'dueDate', 'createdAt', 'title' sortOrder: 'desc', searchQuery: '' }, { computedProperties: { filteredTasks: function() { let tasks = this.get('tasks'); const filter = this.get('filter'); const searchQuery = this.get('searchQuery').toLowerCase(); // Apply search filter if (searchQuery) { tasks = tasks.filter(task => task.title.toLowerCase().includes(searchQuery) || task.description.toLowerCase().includes(searchQuery) || task.tags.some(tag => tag.toLowerCase().includes(searchQuery)) ); } // Apply status filter switch (filter) { case 'active': tasks = tasks.filter(task => !task.completed); break; case 'completed': tasks = tasks.filter(task => task.completed); break; case 'overdue': tasks = tasks.filter(task => { const dueDate = new Date(task.dueDate); return dueDate < new Date() && !task.completed; }); break; } // Apply sorting const sortBy = this.get('sortBy'); const sortOrder = this.get('sortOrder'); tasks.sort((a, b) => { let comparison = 0; switch (sortBy) { case 'priority': comparison = b.priority - a.priority; break; case 'dueDate': const dateA = a.dueDate ? new Date(a.dueDate) : new Date('9999-12-31'); const dateB = b.dueDate ? new Date(b.dueDate) : new Date('9999-12-31'); comparison = dateA - dateB; break; case 'createdAt': comparison = new Date(a.createdAt) - new Date(b.createdAt); break; case 'title': comparison = a.title.localeCompare(b.title); break; } return sortOrder === 'desc' ? -comparison : comparison; }); return tasks; }, taskStats: function() { const tasks = this.get('tasks'); return { total: tasks.length, completed: tasks.filter(task => task.completed).length, active: tasks.filter(task => !task.completed).length, overdue: tasks.filter(task => { const dueDate = new Date(task.dueDate); return dueDate < new Date() && !task.completed; }).length }; } } }); } addTask(taskData) { const tasks = [...this.get('tasks')]; const task = new TaskModel(taskData); tasks.push(task.getData()); this.set('tasks', tasks); return task; } updateTask(taskId, updates) { const tasks = this.get('tasks').map(task => task.id === taskId ? { ...task, ...updates } : task ); this.set('tasks', tasks); } removeTask(taskId) { const tasks = this.get('tasks').filter(task => task.id !== taskId); this.set('tasks', tasks); } getTask(taskId) { return this.get('tasks').find(task => task.id === taskId); } } // ============ TASK VIEW ============ class TaskView extends MusPEView { constructor(options = {}) { super({ template: ` <div class="task-manager"> <header class="task-header"> <h1>Task Manager</h1> <div class="task-stats"> <div class="stat"> <span class="stat-value">{{taskStats.total}}</span> <span class="stat-label">Total</span> </div> <div class="stat"> <span class="stat-value">{{taskStats.active}}</span> <span class="stat-label">Active</span> </div> <div class="stat"> <span class="stat-value">{{taskStats.completed}}</span> <span class="stat-label">Completed</span> </div> <div class="stat"> <span class="stat-value">{{taskStats.overdue}}</span> <span class="stat-label">Overdue</span> </div> </div> </header> <div class="task-controls"> <div class="search-container"> <input type="text" class="search-input" placeholder="Search tasks..." value="{{searchQuery}}" data-event="input:onSearchChange"> </div> <div class="filter-container"> <select class="filter-select" data-event="change:onFilterChange"> <option value="all" {{#if (eq filter 'all')}}selected{{/if}}>All Tasks</option> <option value="active" {{#if (eq filter 'active')}}selected{{/if}}>Active</option> <option value="completed" {{#if (eq filter 'completed')}}selected{{/if}}>Completed</option> <option value="overdue" {{#if (eq filter 'overdue')}}selected{{/if}}>Overdue</option> </select> </div> <div class="sort-container"> <select class="sort-select" data-event="change:onSortChange"> <option value="priority">Priority</option> <option value="dueDate">Due Date</option> <option value="createdAt">Created</option> <option value="title">Title</option> </select> </div> <button class="btn btn-primary" data-event="click:onNewTask"> + New Task </button> </div> <div class="task-list"> {{#each filteredTasks as task}} <div class="task-item {{#if task.completed}}completed{{/if}} {{#if task.isOverdue}}overdue{{/if}}" data-task-id="{{task.id}}"> <div class="task-content"> <div class="task-checkbox"> <input type="checkbox" {{#if task.completed}}checked{{/if}} data-event="change:onTaskToggle"> </div> <div class="task-info"> <h3 class="task-title">{{task.title}}</h3> {{#if task.description}} <p class="task-description">{{task.description}}</p> {{/if}} <div class="task-meta"> <span class="task-priority priority-{{task.priority}}"> {{task.displayPriority}} </span> {{#if task.dueDate}} <span class="task-due-date">Due: {{formatDate task.dueDate}}</span> {{/if}} {{#if task.tags}} <div class="task-tags"> {{#each task.tags as tag}} <span class="task-tag">{{tag}}</span> {{/each}} </div> {{/if}} </div> {{#if task.subtasks}} <div class="task-progress"> <div class="progress-bar"> <div class="progress-fill" style="width: {{task.progressPercentage}}%"></div> </div> <span class="progress-text">{{task.progressPercentage}}% Complete</span> </div> {{/if}} </div> <div class="task-actions"> <button class="btn btn-small" data-event="click:onEditTask">Edit</button> <button class="btn btn-small btn-danger" data-event="click:onDeleteTask">Delete</button> </div> </div> </div> {{/each}} {{#if (eq filteredTasks.length 0)}} <div class="empty-state"> <p>No tasks found</p> <button class="btn btn-primary" data-event="click:onNewTask"> Create your first task </button> </div> {{/if}} </div> </div> `, ...options }); } getData() { return this.viewData || {}; } setData(data) { this.viewData = data; this.render(data); } // Event handlers onSearchChange(event) { this.emit('search:change', event.target.value); } onFilterChange(event) { this.emit('filter:change', event.target.value); } onSortChange(event) { this.emit('sort:change', event.target.value); } onNewTask() { this.emit('task:new'); } onTaskToggle(event) { const taskItem = event.target.closest('.task-item'); const taskId = taskItem.dataset.taskId; this.emit('task:toggle', { taskId, completed: event.target.checked }); } onEditTask(event) { const taskItem = event.target.closest('.task-item'); const taskId = taskItem.dataset.taskId; this.emit('task:edit', taskId); } onDeleteTask(event) { const taskItem = event.target.closest('.task-item'); const taskId = taskItem.dataset.taskId; this.emit('task:delete', taskId); } // Helper methods for template formatDate(dateString) { return new Date(dateString).toLocaleDateString(); } eq(a, b) { return a === b; } } // ============ TASK CONTROLLER ============ class TaskController extends MusPEController { constructor(options = {}) { super({ enableCaching: true, ...options }); } setupModel() { this.model = new TaskListModel(); // Load saved tasks from localStorage this.loadTasksFromStorage(); // Auto-save when tasks change this.model.on('change', () => { this.saveTasksToStorage(); }); } setupView() { this.view = new TaskView({ container: this.options.container || '#app' }); // Bind view events this.view.on('search:change', (query) => { this.model.set('searchQuery', query); }); this.view.on('filter:change', (filter) => { this.model.set('filter', filter); }); this.view.on('sort:change', (sortBy) => { this.model.set('sortBy', sortBy); }); this.view.on('task:new', () => { this.createNewTask(); }); this.view.on('task:toggle', (data) => { this.toggleTask(data.taskId, data.completed); }); this.view.on('task:edit', (taskId) => { this.editTask(taskId); }); this.view.on('task:delete', (taskId) => { this.deleteTask(taskId); }); } setupServices() { // Task persistence service this.addService('storage', { save: (key, data) => { try { localStorage.setItem(key, JSON.stringify(data)); return true; } catch (error) { console.error('Storage save failed:', error); return false; } }, load: (key) => { try { const data = localStorage.getItem(key); return data ? JSON.parse(data) : null; } catch (error) { console.error('Storage load failed:', error); return null; } } }); // Notification service this.addService('notifications', { show: (message, type = 'info') => { // Simple notification implementation const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } }); } // Action handlers async createNewTask() { const title = prompt('Enter task title:'); if (!title) return; const description = prompt('Enter task description (optional):') || ''; const priority = parseInt(prompt('Enter priority (1-4):') || '2'); const taskData = { title, description, priority: Math.max(1, Math.min(4, priority)) }; try { const task = this.model.addTask(taskData); this.getService('notifications').show('Task created successfully!', 'success'); this.emit('task:created', task); } catch (error) { this.getService('notifications').show('Failed to create task', 'error'); } } toggleTask(taskId, completed) { try { const task = this.model.getTask(taskId); if (task) { this.model.updateTask(taskId, { completed, completedAt: completed ? new Date().toISOString() : null }); const message = completed ? 'Task completed!' : 'Task reopened!'; this.getService('notifications').show(message, 'success'); } } catch (error) { this.getService('notifications').show('Failed to update task', 'error'); } } editTask(taskId) { const task = this.model.getTask(taskId); if (!task) return; const newTitle = prompt('Edit task title:', task.title); if (newTitle && newTitle !== task.title) { this.model.updateTask(taskId, { title: newTitle }); this.getService('notifications').show('Task updated!', 'success'); } } deleteTask(taskId) { if (confirm('Are you sure you want to delete this task?')) { try { this.model.removeTask(taskId); this.getService('notifications').show('Task deleted!', 'success'); } catch (error) { this.getService('notifications').show('Failed to delete task', 'error'); } } } // Data persistence saveTasksToStorage() { const storage = this.getService('storage'); const tasks = this.model.get('tasks'); storage.save('muspe_tasks', tasks); } loadTasksFromStorage() { const storage = this.getService('storage'); const tasks = storage.load('muspe_tasks') || []; this.model.set('tasks', tasks); } // Data layer implementation async fetchData(params) { // Simulate API call return new Promise(resolve => { setTimeout(() => { resolve(this.getService('storage').load('muspe_tasks') || []); }, 100); }); } async persistData(data) { // Simulate API call return new Promise(resolve => { setTimeout(() => { this.getService('storage').save('muspe_tasks', data); resolve({ success: true }); }, 100); }); } } // ============ TASK MANAGER FACTORY ============ class TaskManagerFactory { static create(container = '#app') { const controller = new TaskController({ container }); // Initialize and return the MVC setup return { controller, model: controller.model, view: controller.view, // Public API addTask: (taskData) => controller.model.addTask(taskData), removeTask: (taskId) => controller.model.removeTask(taskId), toggleTask: (taskId, completed) => controller.toggleTask(taskId, completed), getStats: () => controller.model.get('taskStats'), destroy: () => controller.destroy() }; } } // Export for use export { TaskModel, TaskListModel, TaskView, TaskController, TaskManagerFactory }; // Global registration if (typeof window !== 'undefined') { window.TaskManagerFactory = TaskManagerFactory; // Auto-initialize if container exists document.addEventListener('DOMContentLoaded', () => { const container = document.querySelector('#task-manager'); if (container) { window.taskManager = TaskManagerFactory.create('#task-manager'); } }); }