/** * user-store.js * User state management store * Manages current logged-in user, preferences, and integrations * MVP3: Integrates with backend API while maintaining localStorage persistence */ import apiClient from '../services/api-client.js'; export class UserStore { constructor() { // Current user state this.currentUser = this.loadUserFromStorage(); this.isAuthenticated = !!this.currentUser; this.isLoading = false; // User preferences this.preferences = this.loadPreferencesFromStorage() || this.getDefaultPreferences(); // User integrations (API keys, tokens) this.integrations = this.loadIntegrationsFromStorage() || {}; // Event listeners this.listeners = new Set(); // Try to verify current user with API on initialization if (this.isAuthenticated) { this.verifyUserWithAPI(); } } /** * Get default user preferences * @returns {Object} Default preferences */ getDefaultPreferences() { return { theme: 'dark', language: 'en', lastTeam: 'ui', chatCollapsedState: true, notifications: { enabled: true, email: true, desktop: true }, layout: { showChat: true, showPanel: true, sidebarWidth: 250 }, editor: { fontSize: 13, fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace', lineHeight: 1.6 } }; } /** * Load user from localStorage * @returns {Object|null} User object or null */ loadUserFromStorage() { try { const stored = localStorage.getItem('current_user'); return stored ? JSON.parse(stored) : null; } catch (error) { console.warn('[UserStore] Failed to parse stored user:', error); return null; } } /** * Load preferences from localStorage * @returns {Object|null} Preferences or null */ loadPreferencesFromStorage() { try { const stored = localStorage.getItem('user_preferences'); return stored ? JSON.parse(stored) : null; } catch (error) { console.warn('[UserStore] Failed to parse stored preferences:', error); return null; } } /** * Load integrations from localStorage * @returns {Object} Integrations object */ loadIntegrationsFromStorage() { try { const stored = localStorage.getItem('user_integrations'); return stored ? JSON.parse(stored) : {}; } catch (error) { console.warn('[UserStore] Failed to parse stored integrations:', error); return {}; } } /** * Persist user to localStorage */ persistUser() { if (this.currentUser) { localStorage.setItem('current_user', JSON.stringify(this.currentUser)); } else { localStorage.removeItem('current_user'); } } /** * Persist preferences to localStorage */ persistPreferences() { localStorage.setItem('user_preferences', JSON.stringify(this.preferences)); } /** * Persist integrations to localStorage */ persistIntegrations() { localStorage.setItem('user_integrations', JSON.stringify(this.integrations)); } /** * Verify current user with API */ async verifyUserWithAPI() { try { const user = await apiClient.getMe(); this.currentUser = user; this.isAuthenticated = true; this.persistUser(); this.notifyListeners(); } catch (error) { console.warn('[UserStore] Failed to verify user with API:', error); // Fall back to localStorage user } } /** * Register new user * @param {string} email - User email * @param {string} password - User password * @param {string} name - User name * @returns {Object} Created user */ async register(email, password, name) { this.isLoading = true; this.notifyListeners(); try { const user = await apiClient.register(email, password, name); this.currentUser = user; this.isAuthenticated = true; this.persistUser(); this.notifyListeners(); return user; } catch (error) { this.isLoading = false; this.notifyListeners(); throw error; } } /** * Login user * @param {string} email - User email * @param {string} password - User password * @returns {Object} Logged-in user */ async login(email, password) { this.isLoading = true; this.notifyListeners(); try { const user = await apiClient.login(email, password); this.currentUser = user; this.isAuthenticated = true; this.persistUser(); this.isLoading = false; this.notifyListeners(); return user; } catch (error) { this.isLoading = false; this.isAuthenticated = false; this.notifyListeners(); throw error; } } /** * Logout user */ async logout() { try { await apiClient.logout(); } catch (error) { console.warn('[UserStore] API logout failed:', error); } this.currentUser = null; this.isAuthenticated = false; this.integrations = {}; localStorage.removeItem('current_user'); localStorage.removeItem('user_preferences'); localStorage.removeItem('user_integrations'); localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); this.notifyListeners(); } /** * Get current user * @returns {Object|null} Current user or null */ getCurrentUser() { return this.currentUser; } /** * Check if user is authenticated * @returns {boolean} Authentication status */ isLoggedIn() { return this.isAuthenticated; } /** * Update user profile * @param {Object} updates - {name, email, avatar, bio} * @returns {Object} Updated user */ async updateProfile(updates) { try { // Try API first if authenticated if (this.isAuthenticated && apiClient.accessToken) { const updated = await apiClient.updateUser(this.currentUser.id, updates); this.currentUser = { ...this.currentUser, ...updated }; this.persistUser(); this.notifyListeners(); return updated; } } catch (error) { console.warn('[UserStore] API profile update failed:', error); } // Fallback to local update this.currentUser = { ...this.currentUser, ...updates }; this.persistUser(); this.notifyListeners(); return this.currentUser; } /** * Update user preferences * @param {Object} updates - Partial preference updates * @returns {Object} Updated preferences */ updatePreferences(updates) { this.preferences = { ...this.preferences, ...updates }; // Deep merge nested objects if (updates.notifications) { this.preferences.notifications = { ...this.preferences.notifications, ...updates.notifications }; } if (updates.layout) { this.preferences.layout = { ...this.preferences.layout, ...updates.layout }; } if (updates.editor) { this.preferences.editor = { ...this.preferences.editor, ...updates.editor }; } this.persistPreferences(); this.notifyListeners(); return this.preferences; } /** * Get user preferences * @returns {Object} Current preferences */ getPreferences() { return { ...this.preferences }; } /** * Add or update integration * @param {string} service - Service name (figma, jira, github, slack, storybook) * @param {string} apiKey - API key or token for the service * @param {Object} metadata - Additional metadata (e.g., {projectKey: 'ABC'}) * @returns {Object} Updated integrations */ setIntegration(service, apiKey, metadata = {}) { this.integrations[service] = { enabled: !!apiKey, apiKey, ...metadata, lastUpdated: new Date().toISOString() }; this.persistIntegrations(); this.notifyListeners(); return this.integrations; } /** * Get specific integration * @param {string} service - Service name * @returns {Object|null} Integration config or null */ getIntegration(service) { return this.integrations[service] || null; } /** * Get all integrations * @returns {Object} All integrations */ getIntegrations() { return { ...this.integrations }; } /** * Remove integration * @param {string} service - Service name * @returns {Object} Updated integrations */ removeIntegration(service) { delete this.integrations[service]; this.persistIntegrations(); this.notifyListeners(); return this.integrations; } /** * Get user avatar (with fallback) * @returns {string} Avatar URL or initials */ getAvatar() { if (!this.currentUser) return null; if (this.currentUser.avatar) return this.currentUser.avatar; // Generate initials-based avatar const initials = (this.currentUser.name || this.currentUser.email) .split(' ') .map(n => n[0]) .join('') .toUpperCase(); return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect fill='%23007acc' width='32' height='32'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='14' font-weight='bold' font-family='sans-serif' transform='translate(0, 1)'%3E${initials}%3C/text%3E%3C/svg%3E`; } /** * Get user display name * @returns {string} Name or email */ getDisplayName() { if (!this.currentUser) return 'Anonymous'; return this.currentUser.name || this.currentUser.email; } /** * Subscribe to user state changes * @param {Function} callback - Called with {currentUser, preferences, integrations, isAuthenticated} * @returns {Function} Unsubscribe function */ subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } /** * Notify all listeners of state changes */ notifyListeners() { this.listeners.forEach(listener => listener({ currentUser: this.currentUser, preferences: this.getPreferences(), integrations: this.getIntegrations(), isAuthenticated: this.isAuthenticated })); } /** * Reset to default state */ reset() { this.currentUser = null; this.isAuthenticated = false; this.preferences = this.getDefaultPreferences(); this.integrations = {}; localStorage.removeItem('current_user'); localStorage.removeItem('user_preferences'); localStorage.removeItem('user_integrations'); this.notifyListeners(); } } // Singleton instance let userStoreInstance = null; export function useUserStore() { if (!userStoreInstance) { userStoreInstance = new UserStore(); } return userStoreInstance; }