/** * project-store.js * Project configuration store * Manages project list, current project selection, and project metadata * MVP3: Integrates with API backend while maintaining localStorage fallback */ import apiClient from '../services/api-client.js'; export class ProjectStore { constructor() { // Load projects from localStorage (fallback for MVP2) const stored = localStorage.getItem('projects_list'); this.projects = stored ? JSON.parse(stored) : this.getDefaultProjects(); // Load current project selection this.currentProjectId = localStorage.getItem('current_project_id') || (this.projects.length > 0 ? this.projects[0].id : null); this.listeners = new Set(); this.isLoading = false; this.hasLoadedFromAPI = false; // Try to load from API on initialization this.syncWithAPI(); } async syncWithAPI() { try { const projects = await apiClient.getProjects(); this.projects = projects; this.hasLoadedFromAPI = true; this.persist(); this.notifyListeners(); } catch (error) { console.warn('[ProjectStore] Failed to sync with API, using localStorage:', error); // Continue using localStorage data } } getDefaultProjects() { return [ { id: 'DEFAULT-DESIGN-SYSTEM', name: 'Default Design System', skinSelected: 'default', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } ]; } /** * Get all projects * @returns {Array} Projects array */ getProjects() { return [...this.projects]; } /** * Get current project * @returns {Object|null} Current project or null */ getCurrentProject() { return this.projects.find(p => p.id === this.currentProjectId) || null; } /** * Get project by ID * @param {string} id - Project ID * @returns {Object|null} Project or null */ getProject(id) { return this.projects.find(p => p.id === id) || null; } /** * Create new project * @param {Object} projectData - {name, key, description, figmaFileKey, jiraProjectKey, storybookUrl} * @returns {Object} Created project */ async createProject(projectData) { try { // Try API first if authenticated if (apiClient.accessToken) { const project = await apiClient.createProject(projectData); this.projects.push(project); this.persist(); this.notifyListeners(); return project; } } catch (error) { console.warn('[ProjectStore] API creation failed, using localStorage:', error); } // Fallback to localStorage const now = new Date().toISOString(); const project = { id: projectData.id || `PROJECT-${Date.now()}`, name: projectData.name || 'Untitled Project', key: projectData.key || `KEY-${Date.now()}`, description: projectData.description || '', createdAt: now, updatedAt: now, ...projectData }; this.projects.push(project); this.persist(); this.notifyListeners(); return project; } /** * Update project * @param {string} id - Project ID * @param {Object} updates - Partial project updates * @returns {Object|null} Updated project or null */ async updateProject(id, updates) { const project = this.projects.find(p => p.id === id); if (!project) return null; try { // Try API first if authenticated if (apiClient.accessToken) { const updated = await apiClient.updateProject(id, updates); const index = this.projects.findIndex(p => p.id === id); if (index !== -1) { this.projects[index] = updated; } this.persist(); this.notifyListeners(); return updated; } } catch (error) { console.warn('[ProjectStore] API update failed, using localStorage:', error); } // Fallback to localStorage Object.assign(project, updates, { updatedAt: new Date().toISOString() }); this.persist(); this.notifyListeners(); return project; } /** * Delete project * @param {string} id - Project ID * @returns {boolean} Success */ async deleteProject(id) { try { // Try API first if authenticated if (apiClient.accessToken) { await apiClient.deleteProject(id); } } catch (error) { console.warn('[ProjectStore] API deletion failed, using localStorage:', error); } const index = this.projects.findIndex(p => p.id === id); if (index === -1) return false; this.projects.splice(index, 1); // If deleted project was selected, select first available if (this.currentProjectId === id) { this.currentProjectId = this.projects.length > 0 ? this.projects[0].id : null; localStorage.setItem('current_project_id', this.currentProjectId || ''); } this.persist(); this.notifyListeners(); return true; } /** * Select/switch to different project * @param {string} id - Project ID * @returns {Object|null} Selected project or null */ selectProject(id) { const project = this.projects.find(p => p.id === id); if (!project) return null; this.currentProjectId = id; localStorage.setItem('current_project_id', id); this.notifyListeners(); return project; } /** * Update selected skin for current project * @param {string} skin - Skin name * @returns {Object|null} Updated project */ setSkinForCurrentProject(skin) { if (!this.currentProjectId) return null; return this.updateProject(this.currentProjectId, { skinSelected: skin }); } /** * Persist to localStorage */ persist() { localStorage.setItem('projects_list', JSON.stringify(this.projects)); } /** * Subscribe to changes * @param {Function} callback - Called on state changes * @returns {Function} Unsubscribe function */ subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } notifyListeners() { this.listeners.forEach(listener => listener({ projects: this.getProjects(), currentProject: this.getCurrentProject() })); } /** * Reset to default project */ reset() { localStorage.removeItem('projects_list'); localStorage.removeItem('current_project_id'); this.projects = this.getDefaultProjects(); this.currentProjectId = this.projects[0].id; this.notifyListeners(); } } // Singleton instance let projectStoreInstance = null; export function useProjectStore() { if (!projectStoreInstance) { projectStoreInstance = new ProjectStore(); } return projectStoreInstance; }