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
JavaScript
// 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');
}
});
}