From 02d02b42f8dfff22398bfd00e056f127bce4df3d Mon Sep 17 00:00:00 2001 From: Itay Pk Date: Thu, 5 Feb 2026 23:01:31 +0200 Subject: [PATCH 1/5] - Cleanup - remove old JS files - Update settings dialog - remove redundant attributes, add user preferences - Change UI to support email + verification code, instead of username/password --- app/routers/__init__.py | 0 app/routers/settings.py | 40 ++ frontend/css/components/forms.css | 13 + frontend/css/components/modals.css | 10 + frontend/js/api.js | 165 ------- frontend/js/app.js | 372 -------------- frontend/js/auth.js | 291 ----------- frontend/js/components/form-validator.js | 299 ----------- frontend/js/components/legal-modals.js | 225 --------- frontend/js/components/modal.js | 220 --------- frontend/js/components/multi-select.js | 169 ------- frontend/js/components/pagination.js | 165 ------- frontend/js/components/settings-modal.js | 392 --------------- frontend/js/components/theme-dropdown.js | 169 ------- frontend/js/components/theme-picker.js | 217 -------- frontend/js/fixtures/history-data.js | 282 ----------- frontend/js/fixtures/mock-auth.js | 23 - frontend/js/pages/history.js | 575 ---------------------- frontend/js/pages/login.js | 65 --- frontend/js/router.js | 172 ------- frontend/js/utils/dom.js | 224 --------- frontend/js/utils/storage.js | 157 ------ frontend/js/utils/theme.js | 134 ----- frontend/src/components/SettingsModal.tsx | 192 ++++---- frontend/src/lib/auth.ts | 27 + frontend/src/pages/Login.tsx | 148 ++++-- 26 files changed, 280 insertions(+), 4466 deletions(-) create mode 100644 app/routers/__init__.py create mode 100644 app/routers/settings.py delete mode 100644 frontend/js/api.js delete mode 100644 frontend/js/app.js delete mode 100644 frontend/js/auth.js delete mode 100644 frontend/js/components/form-validator.js delete mode 100644 frontend/js/components/legal-modals.js delete mode 100644 frontend/js/components/modal.js delete mode 100644 frontend/js/components/multi-select.js delete mode 100644 frontend/js/components/pagination.js delete mode 100644 frontend/js/components/settings-modal.js delete mode 100644 frontend/js/components/theme-dropdown.js delete mode 100644 frontend/js/components/theme-picker.js delete mode 100644 frontend/js/fixtures/history-data.js delete mode 100644 frontend/js/fixtures/mock-auth.js delete mode 100644 frontend/js/pages/history.js delete mode 100644 frontend/js/pages/login.js delete mode 100644 frontend/js/router.js delete mode 100644 frontend/js/utils/dom.js delete mode 100644 frontend/js/utils/storage.js delete mode 100644 frontend/js/utils/theme.js diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/settings.py b/app/routers/settings.py new file mode 100644 index 0000000..c885532 --- /dev/null +++ b/app/routers/settings.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional +import logging + +router = APIRouter(prefix="/api/settings", tags=["settings"]) +logger = logging.getLogger(__name__) + +class SettingsResponse(BaseModel): + ai_context: Optional[str] = None + auto_sync: bool = True + usage_analytics: bool = True + + +class SettingsUpdate(BaseModel): + ai_context: Optional[str] = None + auto_sync: Optional[bool] = None + usage_analytics: Optional[bool] = None + + +@router.get("", response_model=SettingsResponse) +async def get_settings(): + logger.debug("Fetching user settings - return a stub") + """Fetch user settings. Returns a stub for now.""" + return SettingsResponse( + ai_context="", + auto_sync=True, + usage_analytics=True + ) + + +@router.patch("", response_model=SettingsResponse) +async def update_settings(settings: SettingsUpdate): + """Update user settings. Does nothing for now, just returns the current stub.""" + logger.debug("Updating user settings with request %s" % str(settings)) + return SettingsResponse( + ai_context=settings.ai_context or "", + auto_sync=settings.auto_sync if settings.auto_sync is not None else True, + usage_analytics=settings.usage_analytics if settings.usage_analytics is not None else True + ) diff --git a/frontend/css/components/forms.css b/frontend/css/components/forms.css index 1faed6e..134f03c 100644 --- a/frontend/css/components/forms.css +++ b/frontend/css/components/forms.css @@ -68,6 +68,19 @@ border-color: var(--color-primary); } +/* Textarea Small - for modals and compact layouts */ +.textarea-sm { + min-height: 120px; + padding: 1rem; + font-size: var(--text-base); + font-weight: 400; + line-height: 1.5; +} + +.textarea-sm:focus { + border-color: var(--color-border-hover); +} + /* Text Input */ .form-vertical .form-group input { width: 100%; diff --git a/frontend/css/components/modals.css b/frontend/css/components/modals.css index e5729d5..cbd6399 100644 --- a/frontend/css/components/modals.css +++ b/frontend/css/components/modals.css @@ -178,6 +178,16 @@ background-color: var(--color-surface-highlight); } +/* Ensure elements remain visible when row is hovered */ +.setting-row:hover .textarea { + background-color: var(--color-surface-dark); +} + +.setting-row:hover .btn { + background-color: var(--color-surface-dark); + box-shadow: 0 0 0 1px var(--color-border); +} + .setting-label { display: flex; align-items: center; diff --git a/frontend/js/api.js b/frontend/js/api.js deleted file mode 100644 index 535e70a..0000000 --- a/frontend/js/api.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * API Client for TaskCraft Backend - * Centralized fetch wrapper with authentication and error handling - */ - -class ApiClient { - constructor(baseURL = 'http://localhost:8000/api') { - this.baseURL = baseURL; - this.defaultHeaders = { - 'Content-Type': 'application/json', - }; - } - - /** - * Set authentication token - * @param {string} token - JWT or session token - */ - setAuthToken(token) { - if (token) { - this.defaultHeaders['Authorization'] = `Bearer ${token}`; - } else { - delete this.defaultHeaders['Authorization']; - } - } - - /** - * Get full URL with base path - * @param {string} endpoint - API endpoint - */ - getURL(endpoint) { - return `${this.baseURL}${endpoint}`; - } - - /** - * Handle API response - * @param {Response} response - Fetch response - */ - async handleResponse(response) { - const contentType = response.headers.get('content-type'); - const isJSON = contentType && contentType.includes('application/json'); - - if (!response.ok) { - const error = new Error('API request failed'); - error.status = response.status; - error.statusText = response.statusText; - - if (isJSON) { - error.data = await response.json(); - } else { - error.data = await response.text(); - } - - throw error; - } - - return isJSON ? await response.json() : await response.text(); - } - - /** - * Make a GET request - * @param {string} endpoint - API endpoint - * @param {Object} options - Additional fetch options - */ - async get(endpoint, options = {}) { - const response = await fetch(this.getURL(endpoint), { - method: 'GET', - headers: { ...this.defaultHeaders, ...options.headers }, - ...options, - }); - - return this.handleResponse(response); - } - - /** - * Make a POST request - * @param {string} endpoint - API endpoint - * @param {Object} data - Request body data - * @param {Object} options - Additional fetch options - */ - async post(endpoint, data = null, options = {}) { - const response = await fetch(this.getURL(endpoint), { - method: 'POST', - headers: { ...this.defaultHeaders, ...options.headers }, - body: data ? JSON.stringify(data) : null, - ...options, - }); - - return this.handleResponse(response); - } - - /** - * Make a PUT request - * @param {string} endpoint - API endpoint - * @param {Object} data - Request body data - * @param {Object} options - Additional fetch options - */ - async put(endpoint, data = null, options = {}) { - const response = await fetch(this.getURL(endpoint), { - method: 'PUT', - headers: { ...this.defaultHeaders, ...options.headers }, - body: data ? JSON.stringify(data) : null, - ...options, - }); - - return this.handleResponse(response); - } - - /** - * Make a PATCH request - * @param {string} endpoint - API endpoint - * @param {Object} data - Request body data - * @param {Object} options - Additional fetch options - */ - async patch(endpoint, data = null, options = {}) { - const response = await fetch(this.getURL(endpoint), { - method: 'PATCH', - headers: { ...this.defaultHeaders, ...options.headers }, - body: data ? JSON.stringify(data) : null, - ...options, - }); - - return this.handleResponse(response); - } - - /** - * Make a DELETE request - * @param {string} endpoint - API endpoint - * @param {Object} options - Additional fetch options - */ - async delete(endpoint, options = {}) { - const response = await fetch(this.getURL(endpoint), { - method: 'DELETE', - headers: { ...this.defaultHeaders, ...options.headers }, - ...options, - }); - - return this.handleResponse(response); - } - - /** - * Upload files via multipart/form-data - * @param {string} endpoint - API endpoint - * @param {FormData} formData - FormData object with files - * @param {Object} options - Additional fetch options - */ - async upload(endpoint, formData, options = {}) { - // Don't set Content-Type for multipart/form-data - browser will set it with boundary - const headers = { ...this.defaultHeaders }; - delete headers['Content-Type']; - - const response = await fetch(this.getURL(endpoint), { - method: 'POST', - headers: { ...headers, ...options.headers }, - body: formData, - ...options, - }); - - return this.handleResponse(response); - } -} - -// Create singleton instance -const api = new ApiClient(); - -export default api; diff --git a/frontend/js/app.js b/frontend/js/app.js deleted file mode 100644 index 3d27dd3..0000000 --- a/frontend/js/app.js +++ /dev/null @@ -1,372 +0,0 @@ -/** - * TaskCraft Main Application - * Initializes router, authentication, and app state - */ - -import router from './router.js'; -import auth from './auth.js'; -import api from './api.js'; -import { loadHTML, createElement } from './utils/dom.js'; -import { createThemeDropdown } from './components/theme-dropdown.js'; -import { openSettingsModal } from './components/settings-modal.js'; -import { openPrivacyModal, openTermsModal } from './components/legal-modals.js'; - -/** - * Load and return page content - * @param {string} pageName - Name of the page file - */ -async function loadPage(pageName) { - try { - const html = await loadHTML(`/pages/${pageName}`); - return html; - } catch (error) { - console.error(`Failed to load page: ${pageName}`, error); - return `
Failed to load page. Please try again.
`; - } -} - -/** - * Initialize dashboard page functionality - */ -function initDashboard() { - const generateBtn = document.getElementById('generate-btn'); - const taskInput = document.getElementById('task-input'); - - if (generateBtn && taskInput) { - generateBtn.addEventListener('click', async () => { - const taskText = taskInput.value.trim(); - - if (!taskText) { - alert('Please enter a task'); - return; - } - - try { - generateBtn.disabled = true; - generateBtn.textContent = 'Generating...'; - - // Call API to generate calendar events from task - const user = auth.getUser(); - const payload = { - text: taskText, - email: user ? user.email : null, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Jerusalem" - }; - - const response = await api.post('/tasks', payload); - - console.log('Generated task output:', response); - - // Clear input on success - taskInput.value = ''; - - // TODO: Refresh last processed list - } catch (error) { - console.error('Failed to generate tasks:', error); - alert('Failed to generate tasks. Please try again.'); - } finally { - generateBtn.disabled = false; - - // Rebuild button content safely - generateBtn.textContent = ''; - const icon = createElement('span', { className: 'material-symbols-outlined icon-lg' }, 'auto_awesome'); - const text = createElement('span', {}, 'Generate'); - generateBtn.appendChild(icon); - generateBtn.appendChild(text); - } - }); - - // Handle Enter key - taskInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - generateBtn.click(); - } - }); - } -} - -/** - * Update user menu based on auth state - */ -function updateUserMenu() { - const userMenu = document.getElementById('user-menu'); - - if (!userMenu) return; - - // Clear existing content - userMenu.textContent = ''; - - if (auth.isAuthenticated()) { - const user = auth.getUser(); - - // Add mock mode indicator - if (auth.isMockMode) { - const mockBadge = createElement('span', { - className: 'badge badge-warning', - style: 'margin-right: 0.5rem;', - title: 'Mock authentication active (dev mode)' - }, 'DEMO'); - userMenu.appendChild(mockBadge); - } - - // Create profile picture element safely - const profilePic = createElement('div', { - className: 'profile-picture', - 'data-alt': user.name || 'User' - }); - - // Set background image if available (sanitize URL) - const avatarUrl = user.avatar || '/images/default-avatar.png'; - profilePic.style.backgroundImage = `url('${CSS.escape(avatarUrl)}')`; - - userMenu.appendChild(profilePic); - } else { - const loginLink = createElement('a', { - href: '/login', - 'data-link': '', - className: 'btn btn-sm btn-primary' - }, 'Log In'); - - userMenu.appendChild(loginLink); - } -} - -/** - * Set up route definitions - */ -function setupRoutes() { - router.addRoutes({ - '/': async () => { - const html = await loadPage('dashboard'); - // Run dashboard init after DOM is updated - setTimeout(initDashboard, 0); - return html; - }, - - '/index.html': async () => { - router.navigate('/'); // Redirect to home - return ''; - }, - - '/dashboard': async () => { - router.navigate('/'); // Redirect to home - return ''; - }, - - '/settings': async () => { - // Settings are handled by special click handler (opens modal) - // This route exists for completeness but shouldn't be reached - return ''; - }, - - '/profile': async () => { - return await loadPage('profile'); - }, - - '/history': async () => { - const html = await loadPage('history'); - // Run history page init after DOM is updated - setTimeout(async () => { - const { initHistoryPage } = await import('./pages/history.js'); - initHistoryPage(); - }, 0); - return html; - }, - - '/help': async () => { - return await loadPage('help'); - }, - - '/about': async () => { - return await loadPage('about'); - }, - - '/login': async () => { - if (auth.isAuthenticated()) { - router.navigate('/'); - return ''; - } - const html = await loadPage('login'); - // Initialize login page after DOM update - setTimeout(async () => { - const { initLoginPage } = await import('./pages/login.js'); - initLoginPage(); - }, 0); - return html; - }, - - '/register': async () => { - if (auth.isAuthenticated()) { - router.navigate('/'); - return ''; - } - return await loadPage('register'); - }, - - '/404': () => { - return ` -
-

404

-

Page not found

-
- Go Home -
-
- `; - }, - }); -} - -/** - * Set up auth state listeners - */ -function setupAuthListeners() { - auth.subscribe((event, data) => { - console.log('Auth event:', event, data); - - if (event === 'login') { - updateUserMenu(); - router.navigate('/'); - } else if (event === 'logout') { - updateUserMenu(); - router.navigate('/login'); - } - }); -} - -/** - * Set up router hooks - */ -function setupRouterHooks() { - // Before navigation - check auth for protected routes - router.beforeNavigate((path) => { - const protectedRoutes = ['/settings', '/profile', '/history']; - - if (protectedRoutes.includes(path) && !auth.isAuthenticated()) { - router.navigate('/login'); - return false; // Cancel navigation - } - - return true; - }); - - // After navigation - update page title and scroll to top - router.afterNavigate((path) => { - const pageTitles = { - '/': 'Dashboard', - '/settings': 'Settings', - '/profile': 'Profile', - '/history': 'History', - '/help': 'Help', - '/about': 'About', - '/login': 'Log In', - '/register': 'Sign Up', - }; - - const pageTitle = pageTitles[path] || 'TaskCraft'; - document.title = `${pageTitle} | TaskCraft`; - - // Scroll to top - window.scrollTo(0, 0); - - // Update user menu - updateUserMenu(); - }); -} - -/** - * [MOCK AUTH] Check for mock authentication parameter - */ -async function checkMockAuth() { - const params = router.getQueryParams(); - - // Only allow mock auth in development environments - if (params.mock === 'true' && auth.constructor.isMockEnvironment()) { - if (!auth.isAuthenticated()) { - console.log('[MOCK AUTH] Enabling mock authentication...'); - await auth.loginAsMock(); - - // Clean URL (remove ?mock=true) - const cleanPath = window.location.pathname; - window.history.replaceState(null, null, cleanPath); - } - } -} - -/** - * Set up special link handlers - */ -function setupSpecialHandlers() { - // Intercept settings links to open modal instead of navigating - document.addEventListener('click', (e) => { - const settingsLink = e.target.closest('a[href="/settings"]'); - if (settingsLink && settingsLink.hasAttribute('data-link')) { - e.preventDefault(); - e.stopPropagation(); - openSettingsModal(); - return; - } - - // Handle privacy policy link - const privacyLink = e.target.closest('#privacy-link'); - if (privacyLink) { - e.preventDefault(); - e.stopPropagation(); - openPrivacyModal(); - return; - } - - // Handle terms of service link - const termsLink = e.target.closest('#terms-link'); - if (termsLink) { - e.preventDefault(); - e.stopPropagation(); - openTermsModal(); - return; - } - }); -} - -/** - * Initialize the application - */ -async function init() { - console.log('Initializing TaskCraft...'); - - // Set up routes - setupRoutes(); - - // Set up router hooks - setupRouterHooks(); - - // Set up auth listeners - setupAuthListeners(); - - // Set up special handlers (settings modal, etc.) - setupSpecialHandlers(); - - // Update user menu with current auth state - updateUserMenu(); - - // Add theme dropdown to header - const headerButtons = document.getElementById('auth-buttons'); - if (headerButtons) { - createThemeDropdown(headerButtons); - } - - // Check for mock authentication parameter - await checkMockAuth(); - - // Start router - router.start(); - - console.log('TaskCraft initialized'); -} - -// Initialize app when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); -} else { - init(); -} diff --git a/frontend/js/auth.js b/frontend/js/auth.js deleted file mode 100644 index 9bd0417..0000000 --- a/frontend/js/auth.js +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Authentication Manager - * Handles user authentication state, token storage, and auth-related operations - */ - -import api from './api.js'; - -class AuthManager { - constructor() { - this.user = null; - this.token = null; - this.listeners = []; - this.isMockMode = localStorage.getItem('auth_mock_mode') === 'true'; - - // Load saved auth state from localStorage - this.loadAuthState(); - } - - /** - * Load authentication state from localStorage - */ - loadAuthState() { - const savedToken = localStorage.getItem('auth_token'); - const savedUser = localStorage.getItem('auth_user'); - - if (savedToken && savedUser) { - try { - this.token = savedToken; - this.user = JSON.parse(savedUser); - api.setAuthToken(this.token); - } catch (error) { - console.error('Failed to load auth state:', error); - this.clearAuthState(); - } - } - } - - /** - * Save authentication state to localStorage - */ - saveAuthState() { - if (this.token && this.user) { - localStorage.setItem('auth_token', this.token); - localStorage.setItem('auth_user', JSON.stringify(this.user)); - } - } - - /** - * Clear authentication state from localStorage - */ - clearAuthState() { - this.token = null; - this.user = null; - this.isMockMode = false; - localStorage.removeItem('auth_token'); - localStorage.removeItem('auth_user'); - localStorage.removeItem('auth_mock_mode'); - api.setAuthToken(null); - } - - /** - * Register a new user - * @param {Object} userData - User registration data - */ - async register(userData) { - try { - const response = await api.post('/auth/register', userData); - - if (response.token && response.user) { - this.token = response.token; - this.user = response.user; - this.saveAuthState(); - api.setAuthToken(this.token); - this.notifyListeners('login', this.user); - } - - return response; - } catch (error) { - console.error('Registration failed:', error); - throw error; - } - } - - /** - * Log in a user - * @param {string} email - User email - * @param {string} password - User password - */ - async login(email, password) { - try { - const response = await api.post('/auth/login', { email, password }); - - if (response.token && response.user) { - this.token = response.token; - this.user = response.user; - this.saveAuthState(); - api.setAuthToken(this.token); - this.notifyListeners('login', this.user); - } - - return response; - } catch (error) { - console.error('Login failed:', error); - throw error; - } - } - - /** - * [MOCK AUTH] Log in with mock credentials (development only) - * @param {Object} mockUser - Optional custom mock user (uses default if not provided) - */ - async loginAsMock(mockUser = null) { - const { defaultMockUser, mockToken } = await import('./fixtures/mock-auth.js'); - const user = mockUser || defaultMockUser; - - this.token = mockToken; - this.user = user; - this.isMockMode = true; - - localStorage.setItem('auth_mock_mode', 'true'); - this.saveAuthState(); - api.setAuthToken(this.token); - this.notifyListeners('login', this.user); - - console.warn('[MOCK AUTH] Logged in as:', user.email); - return { token: mockToken, user }; - } - - /** - * Log out the current user - */ - async logout() { - try { - // Optionally call backend logout endpoint - if (this.token) { - await api.post('/auth/logout').catch(() => { - // Ignore errors - logout locally anyway - }); - } - } finally { - const wasLoggedIn = this.isAuthenticated(); - this.clearAuthState(); - - if (wasLoggedIn) { - this.notifyListeners('logout'); - } - } - } - - /** - * Check if user is authenticated - * @returns {boolean} - */ - isAuthenticated() { - return !!this.token && !!this.user; - } - - /** - * Get current user - * @returns {Object|null} - */ - getUser() { - return this.user; - } - - /** - * Get current token - * @returns {string|null} - */ - getToken() { - return this.token; - } - - /** - * Update user profile - * @param {Object} userData - Updated user data - */ - async updateProfile(userData) { - try { - const response = await api.patch('/auth/profile', userData); - - if (response.user) { - this.user = { ...this.user, ...response.user }; - this.saveAuthState(); - this.notifyListeners('profile_update', this.user); - } - - return response; - } catch (error) { - console.error('Profile update failed:', error); - throw error; - } - } - - /** - * Change user password - * @param {string} currentPassword - Current password - * @param {string} newPassword - New password - */ - async changePassword(currentPassword, newPassword) { - try { - const response = await api.post('/auth/change-password', { - current_password: currentPassword, - new_password: newPassword, - }); - - return response; - } catch (error) { - console.error('Password change failed:', error); - throw error; - } - } - - /** - * Request password reset - * @param {string} email - User email - */ - async requestPasswordReset(email) { - try { - const response = await api.post('/auth/password-reset-request', { email }); - return response; - } catch (error) { - console.error('Password reset request failed:', error); - throw error; - } - } - - /** - * Reset password with token - * @param {string} token - Reset token from email - * @param {string} newPassword - New password - */ - async resetPassword(token, newPassword) { - try { - const response = await api.post('/auth/password-reset', { - token, - new_password: newPassword, - }); - - return response; - } catch (error) { - console.error('Password reset failed:', error); - throw error; - } - } - - /** - * Subscribe to auth state changes - * @param {Function} listener - Callback function (event, data) => void - */ - subscribe(listener) { - this.listeners.push(listener); - - // Return unsubscribe function - return () => { - this.listeners = this.listeners.filter(l => l !== listener); - }; - } - - /** - * Notify all listeners of auth state change - * @param {string} event - Event type ('login', 'logout', 'profile_update') - * @param {any} data - Event data - */ - notifyListeners(event, data = null) { - this.listeners.forEach(listener => { - try { - listener(event, data); - } catch (error) { - console.error('Auth listener error:', error); - } - }); - } - - /** - * [MOCK AUTH] Check if running in a development environment - * @returns {boolean} True if running on localhost or local network - */ - static isMockEnvironment() { - const hostname = window.location.hostname; - return hostname === 'localhost' || - hostname === '127.0.0.1' || - hostname.startsWith('192.168.') || - hostname.endsWith('.local'); - } -} - -// Create singleton instance -const auth = new AuthManager(); - -export default auth; diff --git a/frontend/js/components/form-validator.js b/frontend/js/components/form-validator.js deleted file mode 100644 index e0e92fa..0000000 --- a/frontend/js/components/form-validator.js +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Form Validation Component - * Uses native Constraint Validation API + custom validators - */ - -class FormValidator { - constructor(formElement, options = {}) { - this.form = formElement; - this.options = { - validateOnBlur: true, - validateOnInput: false, - showErrorsImmediately: false, - errorClass: 'field-error', - errorMessageClass: 'error-message', - ...options, - }; - - this.customValidators = {}; - this.touched = new Set(); - - this.init(); - } - - /** - * Initialize form validation - */ - init() { - this.form.setAttribute('novalidate', ''); - - // Handle form submission - this.form.addEventListener('submit', (e) => { - e.preventDefault(); - this.handleSubmit(); - }); - - // Handle field blur events - if (this.options.validateOnBlur) { - this.form.querySelectorAll('input, textarea, select').forEach((field) => { - field.addEventListener('blur', () => { - this.touched.add(field.name); - this.validateField(field); - }); - }); - } - - // Handle field input events - if (this.options.validateOnInput) { - this.form.querySelectorAll('input, textarea, select').forEach((field) => { - field.addEventListener('input', () => { - if (this.touched.has(field.name)) { - this.validateField(field); - } - }); - }); - } - } - - /** - * Add custom validation rule - * @param {string} fieldName - Name of the field - * @param {Function} validator - Validation function (value) => boolean | string - */ - addRule(fieldName, validator) { - if (!this.customValidators[fieldName]) { - this.customValidators[fieldName] = []; - } - this.customValidators[fieldName].push(validator); - } - - /** - * Validate a single field - * @param {HTMLElement} field - Input field element - */ - validateField(field) { - // Clear previous errors - this.clearFieldError(field); - - // Check native validation first - if (!field.checkValidity()) { - this.showFieldError(field, field.validationMessage); - return false; - } - - // Check custom validators - const customValidators = this.customValidators[field.name]; - if (customValidators) { - for (const validator of customValidators) { - const result = validator(field.value, this.getFormData()); - - if (result !== true) { - const errorMessage = typeof result === 'string' ? result : 'Invalid value'; - this.showFieldError(field, errorMessage); - return false; - } - } - } - - return true; - } - - /** - * Validate entire form - */ - validateForm() { - let isValid = true; - const fields = this.form.querySelectorAll('input, textarea, select'); - - fields.forEach((field) => { - this.touched.add(field.name); - if (!this.validateField(field)) { - isValid = false; - } - }); - - return isValid; - } - - /** - * Show error message for a field - * @param {HTMLElement} field - Input field - * @param {string} message - Error message - */ - showFieldError(field, message) { - field.classList.add(this.options.errorClass); - field.setAttribute('aria-invalid', 'true'); - - // Create or update error message element - let errorElement = field.parentElement.querySelector(`.${this.options.errorMessageClass}`); - - if (!errorElement) { - errorElement = document.createElement('div'); - errorElement.className = this.options.errorMessageClass; - errorElement.setAttribute('role', 'alert'); - field.parentElement.appendChild(errorElement); - } - - errorElement.textContent = message; - } - - /** - * Clear error message for a field - * @param {HTMLElement} field - Input field - */ - clearFieldError(field) { - field.classList.remove(this.options.errorClass); - field.setAttribute('aria-invalid', 'false'); - - const errorElement = field.parentElement.querySelector(`.${this.options.errorMessageClass}`); - if (errorElement) { - errorElement.remove(); - } - } - - /** - * Clear all errors - */ - clearAllErrors() { - this.form.querySelectorAll('input, textarea, select').forEach((field) => { - this.clearFieldError(field); - }); - } - - /** - * Get form data as object - */ - getFormData() { - const formData = new FormData(this.form); - const data = {}; - - for (const [key, value] of formData.entries()) { - data[key] = value; - } - - return data; - } - - /** - * Handle form submission - */ - async handleSubmit() { - if (this.validateForm()) { - const data = this.getFormData(); - - if (this.options.onSubmit) { - try { - await this.options.onSubmit(data); - } catch (error) { - if (this.options.onError) { - this.options.onError(error); - } - } - } - } else { - // Focus first invalid field - const firstInvalid = this.form.querySelector(`.${this.options.errorClass}`); - if (firstInvalid) { - firstInvalid.focus(); - } - } - } - - /** - * Reset form and clear all validation state - */ - reset() { - this.form.reset(); - this.clearAllErrors(); - this.touched.clear(); - } -} - -// Common validation helpers -const validators = { - /** - * Email validator - */ - email: (value) => { - const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return regex.test(value) || 'Please enter a valid email address'; - }, - - /** - * Password strength validator - */ - passwordStrength: (minLength = 8) => { - return (value) => { - if (value.length < minLength) { - return `Password must be at least ${minLength} characters`; - } - if (!/[A-Z]/.test(value)) { - return 'Password must contain at least one uppercase letter'; - } - if (!/[a-z]/.test(value)) { - return 'Password must contain at least one lowercase letter'; - } - if (!/[0-9]/.test(value)) { - return 'Password must contain at least one number'; - } - return true; - }; - }, - - /** - * Password match validator - */ - passwordMatch: (passwordFieldName) => { - return (value, formData) => { - return value === formData[passwordFieldName] || 'Passwords do not match'; - }; - }, - - /** - * Required field validator - */ - required: (value) => { - return value.trim().length > 0 || 'This field is required'; - }, - - /** - * Min length validator - */ - minLength: (length) => { - return (value) => { - return value.length >= length || `Must be at least ${length} characters`; - }; - }, - - /** - * Max length validator - */ - maxLength: (length) => { - return (value) => { - return value.length <= length || `Must be no more than ${length} characters`; - }; - }, - - /** - * Pattern validator - */ - pattern: (regex, message) => { - return (value) => { - return regex.test(value) || message; - }; - }, - - /** - * URL validator - */ - url: (value) => { - try { - new URL(value); - return true; - } catch { - return 'Please enter a valid URL'; - } - }, -}; - -export { FormValidator, validators }; diff --git a/frontend/js/components/legal-modals.js b/frontend/js/components/legal-modals.js deleted file mode 100644 index 4a6a031..0000000 --- a/frontend/js/components/legal-modals.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Legal Modals Component - * Modals for Terms of Service and Privacy Policy - */ - -import { createModal } from './modal.js'; -import { createElement } from '../utils/dom.js'; - -/** - * Open Terms of Service modal - */ -export function openTermsModal() { - const content = createTermsContent(); - - const modal = createModal({ - title: 'Terms of Service', - content: content, - size: 'large' - }); - - modal.open(); -} - -/** - * Open Privacy Policy modal - */ -export function openPrivacyModal() { - const content = createPrivacyContent(); - - const modal = createModal({ - title: 'Privacy Policy', - content: content, - size: 'large' - }); - - modal.open(); -} - -/** - * Create Terms of Service content - */ -function createTermsContent() { - const container = createElement('div', { - className: 'legal-content' - }); - - const lastUpdated = createElement('p', { - className: 'legal-updated' - }, 'Last Updated: January 7, 2026'); - - const intro = createElement('p', { - className: 'legal-intro' - }, 'Welcome to TaskCraft. By using our service, you agree to these terms. Please read them carefully.'); - - container.appendChild(lastUpdated); - container.appendChild(intro); - - // Sections - const sections = [ - { - title: '1. Acceptance of Terms', - content: 'By accessing and using TaskCraft, you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to these terms, please do not use our service.' - }, - { - title: '2. Use of Service', - content: 'TaskCraft provides AI-powered task management and calendar integration services. You agree to use the service only for lawful purposes and in accordance with these Terms of Service.' - }, - { - title: '3. User Accounts', - content: 'You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. You must notify us immediately of any unauthorized use of your account.' - }, - { - title: '4. User Content', - content: 'You retain all rights to the content you submit to TaskCraft. By submitting content, you grant us a license to use, process, and display that content solely for the purpose of providing our services to you.' - }, - { - title: '5. AI Processing', - content: 'Our AI processes your input to create tasks, events, and reminders. While we strive for accuracy, you acknowledge that AI-generated results may require review and correction.' - }, - { - title: '6. Prohibited Activities', - content: 'You may not use TaskCraft to: (a) violate any laws or regulations; (b) infringe on intellectual property rights; (c) transmit malicious code or spam; (d) attempt to gain unauthorized access to our systems; or (e) interfere with other users\' access to the service.' - }, - { - title: '7. Service Availability', - content: 'We strive to maintain high availability but do not guarantee uninterrupted access to TaskCraft. We may suspend or terminate the service for maintenance, updates, or other necessary purposes.' - }, - { - title: '8. Intellectual Property', - content: 'The TaskCraft platform, including its design, features, and content, is protected by copyright, trademark, and other intellectual property laws. You may not copy, modify, or distribute our intellectual property without permission.' - }, - { - title: '9. Limitation of Liability', - content: 'TaskCraft is provided "as is" without warranties of any kind. We are not liable for any indirect, incidental, special, or consequential damages arising from your use of the service.' - }, - { - title: '10. Changes to Terms', - content: 'We reserve the right to modify these terms at any time. We will notify users of significant changes via email or through the service. Continued use of TaskCraft after changes constitutes acceptance of the modified terms.' - }, - { - title: '11. Termination', - content: 'You may terminate your account at any time through the settings page. We reserve the right to suspend or terminate accounts that violate these terms.' - }, - { - title: '12. Governing Law', - content: 'These Terms of Service are governed by and construed in accordance with applicable laws. Any disputes will be resolved in the appropriate courts.' - }, - { - title: '13. Contact Information', - content: 'If you have questions about these terms, please contact us at legal@taskcraft.com.' - } - ]; - - sections.forEach(section => { - const sectionEl = createLegalSection(section.title, section.content); - container.appendChild(sectionEl); - }); - - return container; -} - -/** - * Create Privacy Policy content - */ -function createPrivacyContent() { - const container = createElement('div', { - className: 'legal-content' - }); - - const lastUpdated = createElement('p', { - className: 'legal-updated' - }, 'Last Updated: January 7, 2026'); - - const intro = createElement('p', { - className: 'legal-intro' - }, 'At TaskCraft, we take your privacy seriously. This Privacy Policy explains how we collect, use, and protect your personal information.'); - - container.appendChild(lastUpdated); - container.appendChild(intro); - - // Sections - const sections = [ - { - title: '1. Information We Collect', - content: 'We collect information you provide directly to us, including: account information (name, email, password), task and event data you input, calendar integration details, usage analytics and feedback, and device and browser information.' - }, - { - title: '2. How We Use Your Information', - content: 'We use your information to: provide and improve our services, process your tasks using AI, sync with your calendar, send notifications and reminders, respond to your support requests, analyze usage patterns to enhance features, and comply with legal obligations.' - }, - { - title: '3. AI Processing', - content: 'Your task input is processed by our AI to extract meaningful information such as dates, times, people, and actions. This processing happens securely and your data is not used to train AI models available to other users.' - }, - { - title: '4. Data Storage and Security', - content: 'We use industry-standard encryption (TLS/SSL) to protect data in transit. Data at rest is encrypted using AES-256. We implement access controls, regular security audits, and automated backups. However, no system is completely secure, and we cannot guarantee absolute security.' - }, - { - title: '5. Data Sharing', - content: 'We do not sell your personal information. We may share your data with: service providers who help us operate TaskCraft (under strict confidentiality agreements), calendar services you choose to integrate with, and law enforcement if required by law or to protect our rights.' - }, - { - title: '6. Calendar Integration', - content: 'When you connect your calendar, we request only the permissions necessary to create and modify events. We do not access your entire calendar history unless explicitly required for a feature you enable.' - }, - { - title: '7. Cookies and Tracking', - content: 'We use cookies and similar technologies to: maintain your login session, remember your preferences (theme, settings), analyze usage patterns, and improve performance. You can control cookies through your browser settings.' - }, - { - title: '8. Your Rights', - content: 'You have the right to: access your personal data, correct inaccurate information, delete your account and data, export your data, opt out of marketing communications, and withdraw consent for data processing.' - }, - { - title: '9. Data Retention', - content: 'We retain your data for as long as your account is active. When you delete your account, we permanently delete your personal data within 30 days, except where we are legally required to retain it.' - }, - { - title: '10. Children\'s Privacy', - content: 'TaskCraft is not intended for users under 13 years of age. We do not knowingly collect information from children. If we learn we have collected data from a child, we will delete it immediately.' - }, - { - title: '11. International Data Transfers', - content: 'Your information may be transferred to and processed in countries other than your own. We ensure appropriate safeguards are in place to protect your data in accordance with this Privacy Policy.' - }, - { - title: '12. Changes to This Policy', - content: 'We may update this Privacy Policy from time to time. We will notify you of significant changes via email or through the service. Please review this policy periodically.' - }, - { - title: '13. Contact Us', - content: 'If you have questions about this Privacy Policy or how we handle your data, please contact us at privacy@taskcraft.com or through our support channels.' - } - ]; - - sections.forEach(section => { - const sectionEl = createLegalSection(section.title, section.content); - container.appendChild(sectionEl); - }); - - return container; -} - -/** - * Create a legal section - */ -function createLegalSection(title, content) { - const section = createElement('div', { - className: 'legal-section' - }); - - const titleEl = createElement('h3', { - className: 'legal-section-title' - }, title); - - const contentEl = createElement('p', { - className: 'legal-section-content' - }, content); - - section.appendChild(titleEl); - section.appendChild(contentEl); - - return section; -} diff --git a/frontend/js/components/modal.js b/frontend/js/components/modal.js deleted file mode 100644 index d11f234..0000000 --- a/frontend/js/components/modal.js +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Modal Component - * Reusable modal dialog with overlay - * - * SECURITY NOTE: Modal content uses template elements for HTML parsing. - * Only pass trusted content (e.g., from your own app, not user-generated). - * For user-generated content, sanitize first or use textContent. - */ - -import { createElement } from '../utils/dom.js'; - -class Modal { - constructor(options = {}) { - this.options = { - title: '', - content: '', - size: 'medium', // 'small', 'medium', 'large', 'full' - closeOnEscape: true, - closeOnOverlay: true, - onClose: null, - ...options - }; - - this.isOpen = false; - this.overlay = null; - this.container = null; - - this.handleEscape = this.handleEscape.bind(this); - this.handleOverlayClick = this.handleOverlayClick.bind(this); - } - - /** - * Create and render the modal - */ - create() { - // Create overlay - this.overlay = createElement('div', { - className: 'modal-overlay', - onclick: this.handleOverlayClick - }); - - // Create container - const sizeClass = `modal-${this.options.size}`; - this.container = createElement('div', { - className: `modal-container ${sizeClass}` - }); - - // Create header - const header = createElement('div', { - className: 'modal-header' - }); - - const title = createElement('h2', { - className: 'modal-title' - }, this.options.title); - - const closeBtn = createElement('button', { - className: 'modal-close', - onclick: (e) => { - e.stopPropagation(); - this.close(); - }, - title: 'Close (ESC)' - }); - - const closeIcon = createElement('span', { - className: 'material-symbols-outlined' - }, 'close'); - - closeBtn.appendChild(closeIcon); - header.appendChild(title); - header.appendChild(closeBtn); - - // Create body - const body = createElement('div', { - className: 'modal-body' - }); - - // Set content (can be string or HTMLElement) - if (typeof this.options.content === 'string') { - // Using template element to safely parse trusted HTML from our app - const template = document.createElement('template'); - template.innerHTML = this.options.content; - body.appendChild(template.content); - } else if (this.options.content instanceof HTMLElement) { - body.appendChild(this.options.content); - } - - // Assemble modal - this.container.appendChild(header); - this.container.appendChild(body); - this.overlay.appendChild(this.container); - - return this.overlay; - } - - /** - * Open the modal - */ - open() { - if (this.isOpen) return; - - if (!this.overlay) { - this.create(); - } - - document.body.appendChild(this.overlay); - document.body.style.overflow = 'hidden'; // Prevent background scroll - - // Add event listeners - if (this.options.closeOnEscape) { - document.addEventListener('keydown', this.handleEscape); - } - - this.isOpen = true; - - // Trigger animation - setTimeout(() => { - this.overlay.classList.add('modal-open'); - }, 10); - } - - /** - * Close the modal - */ - close() { - if (!this.isOpen) return; - - this.overlay.classList.remove('modal-open'); - - // Wait for animation before removing - setTimeout(() => { - if (this.overlay && this.overlay.parentNode) { - this.overlay.parentNode.removeChild(this.overlay); - } - - document.body.style.overflow = ''; - document.removeEventListener('keydown', this.handleEscape); - - this.isOpen = false; - - // Call onClose callback - if (this.options.onClose) { - this.options.onClose(); - } - }, 200); // Match CSS transition duration - } - - /** - * Handle ESC key - */ - handleEscape(e) { - if (e.key === 'Escape') { - this.close(); - } - } - - /** - * Handle click on overlay (outside modal) - */ - handleOverlayClick(e) { - if (this.options.closeOnOverlay && e.target === this.overlay) { - this.close(); - } - } - - /** - * Update modal content - * @param {string|HTMLElement} content - New content (trusted only!) - */ - setContent(content) { - if (!this.container) return; - - const body = this.container.querySelector('.modal-body'); - if (!body) return; - - body.textContent = ''; - - if (typeof content === 'string') { - // Using template element for trusted HTML content - const template = document.createElement('template'); - template.innerHTML = content; - body.appendChild(template.content); - } else if (content instanceof HTMLElement) { - body.appendChild(content); - } - } - - /** - * Update modal title - */ - setTitle(title) { - if (!this.container) return; - - const titleEl = this.container.querySelector('.modal-title'); - if (titleEl) { - titleEl.textContent = title; - } - } - - /** - * Destroy modal and clean up - */ - destroy() { - this.close(); - this.overlay = null; - this.container = null; - } -} - -/** - * Create a modal instance - * @param {Object} options - Modal options - * @returns {Modal} Modal instance - */ -export function createModal(options) { - return new Modal(options); -} - -export default Modal; diff --git a/frontend/js/components/multi-select.js b/frontend/js/components/multi-select.js deleted file mode 100644 index 1c9c961..0000000 --- a/frontend/js/components/multi-select.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Multi-Select Dropdown Component - * A modern, accessible multi-select dropdown with chip-style selected items - */ - -/** - * Create a multi-select dropdown - * @param {Object} config - Configuration object - * @param {string} config.id - Unique ID for the dropdown - * @param {string} config.label - Label for the dropdown - * @param {Array} config.options - Array of {value, label, color} objects - * @param {Array} config.defaultSelected - Array of default selected values - * @param {Function} config.onChange - Callback when selection changes - * @returns {HTMLElement} The dropdown container element - */ -export function createMultiSelect(config) { - const { - id, - label, - options = [], - defaultSelected = [], - onChange = () => {} - } = config; - - const selected = new Set(defaultSelected); - - // Container - const container = document.createElement('div'); - container.className = 'multi-select'; - container.id = id; - - // Trigger button - const trigger = document.createElement('button'); - trigger.type = 'button'; - trigger.className = 'multi-select-trigger'; - trigger.setAttribute('aria-haspopup', 'listbox'); - trigger.setAttribute('aria-expanded', 'false'); - - const triggerContent = document.createElement('span'); - triggerContent.className = 'multi-select-trigger-content'; - updateTriggerText(); - - const chevron = document.createElement('span'); - chevron.className = 'material-symbols-outlined multi-select-chevron'; - chevron.textContent = 'expand_more'; - - trigger.appendChild(triggerContent); - trigger.appendChild(chevron); - - // Dropdown menu - const dropdown = document.createElement('div'); - dropdown.className = 'multi-select-dropdown'; - dropdown.setAttribute('role', 'listbox'); - dropdown.setAttribute('aria-multiselectable', 'true'); - - // Options list - options.forEach(option => { - const optionEl = document.createElement('div'); - optionEl.className = 'multi-select-option'; - optionEl.setAttribute('role', 'option'); - optionEl.setAttribute('data-value', option.value); - optionEl.setAttribute('aria-selected', selected.has(option.value)); - - if (selected.has(option.value)) { - optionEl.classList.add('selected'); - } - - const checkbox = document.createElement('div'); - checkbox.className = 'multi-select-checkbox'; - - const checkIcon = document.createElement('span'); - checkIcon.className = 'material-symbols-outlined'; - checkIcon.textContent = 'check'; - checkbox.appendChild(checkIcon); - - const labelEl = document.createElement('span'); - labelEl.textContent = option.label; - - if (option.color) { - const badge = document.createElement('span'); - badge.className = `badge badge-${option.color}`; - badge.textContent = option.label; - optionEl.appendChild(checkbox); - optionEl.appendChild(badge); - } else { - optionEl.appendChild(checkbox); - optionEl.appendChild(labelEl); - } - - optionEl.addEventListener('click', (e) => { - e.stopPropagation(); - toggleOption(option.value); - }); - - dropdown.appendChild(optionEl); - }); - - container.appendChild(trigger); - container.appendChild(dropdown); - - // Toggle dropdown - let isOpen = false; - trigger.addEventListener('click', (e) => { - e.stopPropagation(); - isOpen = !isOpen; - container.classList.toggle('open', isOpen); - trigger.setAttribute('aria-expanded', isOpen); - }); - - // Close on outside click - document.addEventListener('click', (e) => { - if (!container.contains(e.target) && isOpen) { - isOpen = false; - container.classList.remove('open'); - trigger.setAttribute('aria-expanded', 'false'); - } - }); - - function toggleOption(value) { - if (selected.has(value)) { - selected.delete(value); - } else { - selected.add(value); - } - - // Update UI - const optionEl = dropdown.querySelector(`[data-value="${value}"]`); - if (optionEl) { - optionEl.classList.toggle('selected'); - optionEl.setAttribute('aria-selected', selected.has(value)); - } - - updateTriggerText(); - onChange(Array.from(selected)); - } - - function updateTriggerText() { - const count = selected.size; - if (count === 0) { - triggerContent.textContent = `Select ${label}`; - triggerContent.classList.add('placeholder'); - } else if (count === options.length) { - triggerContent.textContent = `All ${label}`; - triggerContent.classList.remove('placeholder'); - } else { - const selectedOptions = options.filter(o => selected.has(o.value)); - triggerContent.textContent = selectedOptions.map(o => o.label).join(', '); - triggerContent.classList.remove('placeholder'); - } - } - - // Public API - container.getSelected = () => Array.from(selected); - container.setSelected = (values) => { - selected.clear(); - values.forEach(v => selected.add(v)); - - dropdown.querySelectorAll('.multi-select-option').forEach(opt => { - const value = opt.getAttribute('data-value'); - const isSelected = selected.has(value); - opt.classList.toggle('selected', isSelected); - opt.setAttribute('aria-selected', isSelected); - }); - - updateTriggerText(); - }; - - return container; -} diff --git a/frontend/js/components/pagination.js b/frontend/js/components/pagination.js deleted file mode 100644 index 4732a48..0000000 --- a/frontend/js/components/pagination.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Pagination Component - * Handles page navigation for lists - */ - -import { createElement } from '../utils/dom.js'; - -/** - * Create pagination controls - * @param {HTMLElement} container - Container element to render into - * @param {Object} options - Pagination options - */ -export function createPagination(container, options = {}) { - const { - totalItems = 0, - itemsPerPage = 20, - currentPage = 1, - maxVisiblePages = 5, - onPageChange = () => {} - } = options; - - const totalPages = Math.ceil(totalItems / itemsPerPage); - - // Clear container - container.textContent = ''; - - if (totalPages <= 1) { - // Don't show pagination if only one page - return; - } - - // Create pagination wrapper - const pagination = createElement('div', { - className: 'pagination' - }); - - // Previous button - const prevBtn = createElement('button', { - className: `pagination-btn pagination-prev ${currentPage === 1 ? 'disabled' : ''}`, - disabled: currentPage === 1, - onclick: () => { - if (currentPage > 1) { - onPageChange(currentPage - 1); - } - } - }); - - const prevIcon = createElement('span', { - className: 'material-symbols-outlined icon-md' - }, 'chevron_left'); - - const prevText = createElement('span', {}, 'Previous'); - - prevBtn.appendChild(prevIcon); - prevBtn.appendChild(prevText); - - // Page numbers - const pages = createElement('div', { - className: 'pagination-pages' - }); - - const pageNumbers = getPageNumbers(currentPage, totalPages, maxVisiblePages); - - pageNumbers.forEach(page => { - if (page === '...') { - const ellipsis = createElement('span', { - className: 'pagination-ellipsis' - }, '...'); - pages.appendChild(ellipsis); - } else { - const pageBtn = createElement('button', { - className: `pagination-page ${page === currentPage ? 'active' : ''}`, - onclick: () => onPageChange(page) - }, String(page)); - pages.appendChild(pageBtn); - } - }); - - // Next button - const nextBtn = createElement('button', { - className: `pagination-btn pagination-next ${currentPage === totalPages ? 'disabled' : ''}`, - disabled: currentPage === totalPages, - onclick: () => { - if (currentPage < totalPages) { - onPageChange(currentPage + 1); - } - } - }); - - const nextText = createElement('span', {}, 'Next'); - - const nextIcon = createElement('span', { - className: 'material-symbols-outlined icon-md' - }, 'chevron_right'); - - nextBtn.appendChild(nextText); - nextBtn.appendChild(nextIcon); - - // Page info - const pageInfo = createElement('div', { - className: 'pagination-info' - }, `Page ${currentPage} of ${totalPages}`); - - // Assemble pagination - pagination.appendChild(prevBtn); - pagination.appendChild(pages); - pagination.appendChild(nextBtn); - pagination.appendChild(pageInfo); - - container.appendChild(pagination); -} - -/** - * Calculate which page numbers to show - * @param {number} current - Current page - * @param {number} total - Total pages - * @param {number} maxVisible - Maximum visible page buttons - * @returns {Array} Array of page numbers and ellipsis - */ -function getPageNumbers(current, total, maxVisible) { - if (total <= maxVisible) { - // Show all pages - return Array.from({ length: total }, (_, i) => i + 1); - } - - const pages = []; - const halfVisible = Math.floor(maxVisible / 2); - - // Always show first page - pages.push(1); - - let start = Math.max(2, current - halfVisible); - let end = Math.min(total - 1, current + halfVisible); - - // Adjust if at beginning or end - if (current <= halfVisible) { - end = maxVisible - 1; - } else if (current >= total - halfVisible) { - start = total - maxVisible + 2; - } - - // Add ellipsis after first page if needed - if (start > 2) { - pages.push('...'); - } - - // Add middle pages - for (let i = start; i <= end; i++) { - pages.push(i); - } - - // Add ellipsis before last page if needed - if (end < total - 1) { - pages.push('...'); - } - - // Always show last page - if (total > 1) { - pages.push(total); - } - - return pages; -} - -export default createPagination; diff --git a/frontend/js/components/settings-modal.js b/frontend/js/components/settings-modal.js deleted file mode 100644 index 0e42363..0000000 --- a/frontend/js/components/settings-modal.js +++ /dev/null @@ -1,392 +0,0 @@ -/** - * Settings Modal Component - * Large modal for application settings - */ - -import { createModal } from './modal.js'; -import { createElement } from '../utils/dom.js'; -import { local } from '../utils/storage.js'; - -/** - * Open the settings modal - */ -export function openSettingsModal() { - const content = createSettingsContent(); - - const modal = createModal({ - title: 'Settings', - content: content, - size: 'large' - }); - - modal.open(); -} - -/** - * Create settings content - */ -function createSettingsContent() { - const container = createElement('div', { - className: 'settings-content' - }); - - // Settings sections - const sections = [ - createAccountSection(), - createNotificationsSection(), - createCalendarSection(), - createAppearanceSection(), - createPrivacySection() - ]; - - sections.forEach(section => container.appendChild(section)); - - return container; -} - -/** - * Account Settings Section - */ -function createAccountSection() { - const section = createElement('div', { className: 'settings-section' }); - - const header = createElement('h3', { className: 'settings-section-title' }, 'Account'); - - const content = createElement('div', { className: 'settings-section-content' }); - - // Email - const emailRow = createSettingRow( - 'Email Address', - 'demo@taskcraft.com', - 'email', - false - ); - - // Name - const nameRow = createSettingRow( - 'Display Name', - 'Demo User', - 'person', - false - ); - - // Change Password Button - const passwordRow = createElement('div', { className: 'setting-row' }); - const passwordLabel = createElement('div', { className: 'setting-label' }); - const passwordIcon = createElement('span', { className: 'material-symbols-outlined icon-md' }, 'lock'); - const passwordText = createElement('div', {}); - const passwordTitle = createElement('div', { className: 'setting-title' }, 'Password'); - const passwordDesc = createElement('div', { className: 'setting-description' }, 'Change your account password'); - passwordText.appendChild(passwordTitle); - passwordText.appendChild(passwordDesc); - passwordLabel.appendChild(passwordIcon); - passwordLabel.appendChild(passwordText); - - const passwordBtn = createElement('button', { - className: 'btn btn-sm btn-secondary' - }, 'Change Password'); - - passwordRow.appendChild(passwordLabel); - passwordRow.appendChild(passwordBtn); - - content.appendChild(emailRow); - content.appendChild(nameRow); - content.appendChild(passwordRow); - - section.appendChild(header); - section.appendChild(content); - - return section; -} - -/** - * Notifications Section - */ -function createNotificationsSection() { - const section = createElement('div', { className: 'settings-section' }); - - const header = createElement('h3', { className: 'settings-section-title' }, 'Notifications'); - - const content = createElement('div', { className: 'settings-section-content' }); - - // Enable notifications - const notifRow = createSettingToggle( - 'Enable Notifications', - 'Receive notifications for upcoming events and reminders', - 'notifications', - 'enable_notifications', - true - ); - - // Email notifications - const emailRow = createSettingToggle( - 'Email Notifications', - 'Get email summaries of your daily tasks', - 'email', - 'email_notifications', - false - ); - - // Sound - const soundRow = createSettingToggle( - 'Notification Sound', - 'Play a sound when you receive notifications', - 'volume_up', - 'notification_sound', - true - ); - - content.appendChild(notifRow); - content.appendChild(emailRow); - content.appendChild(soundRow); - - section.appendChild(header); - section.appendChild(content); - - return section; -} - -/** - * Calendar Integration Section - */ -function createCalendarSection() { - const section = createElement('div', { className: 'settings-section' }); - - const header = createElement('h3', { className: 'settings-section-title' }, 'Calendar Integration'); - - const content = createElement('div', { className: 'settings-section-content' }); - - // Connected calendar - const calendarRow = createElement('div', { className: 'setting-row' }); - const calendarLabel = createElement('div', { className: 'setting-label' }); - const calendarIcon = createElement('span', { className: 'material-symbols-outlined icon-md' }, 'event'); - const calendarText = createElement('div', {}); - const calendarTitle = createElement('div', { className: 'setting-title' }, 'Connected Calendar'); - const calendarDesc = createElement('div', { className: 'setting-description' }, 'Google Calendar - demo@taskcraft.com'); - calendarText.appendChild(calendarTitle); - calendarText.appendChild(calendarDesc); - calendarLabel.appendChild(calendarIcon); - calendarLabel.appendChild(calendarText); - - const calendarBtn = createElement('button', { - className: 'btn btn-sm btn-secondary' - }, 'Disconnect'); - - calendarRow.appendChild(calendarLabel); - calendarRow.appendChild(calendarBtn); - - // Default calendar - const defaultRow = createSettingRow( - 'Default Calendar', - 'Primary', - 'calendar_today', - false - ); - - // Auto-sync - const syncRow = createSettingToggle( - 'Auto-Sync', - 'Automatically sync events to your calendar', - 'sync', - 'auto_sync', - true - ); - - content.appendChild(calendarRow); - content.appendChild(defaultRow); - content.appendChild(syncRow); - - section.appendChild(header); - section.appendChild(content); - - return section; -} - -/** - * Appearance Section - */ -function createAppearanceSection() { - const section = createElement('div', { className: 'settings-section' }); - - const header = createElement('h3', { className: 'settings-section-title' }, 'Appearance'); - - const content = createElement('div', { className: 'settings-section-content' }); - - // Theme selector - const themeRow = createElement('div', { className: 'setting-row' }); - const themeLabel = createElement('div', { className: 'setting-label' }); - const themeIcon = createElement('span', { className: 'material-symbols-outlined icon-md' }, 'palette'); - const themeText = createElement('div', {}); - const themeTitle = createElement('div', { className: 'setting-title' }, 'Theme'); - const themeDesc = createElement('div', { className: 'setting-description' }, 'Choose your color theme from the header'); - themeText.appendChild(themeTitle); - themeText.appendChild(themeDesc); - themeLabel.appendChild(themeIcon); - themeLabel.appendChild(themeText); - - themeRow.appendChild(themeLabel); - - // Compact mode - const compactRow = createSettingToggle( - 'Compact Mode', - 'Reduce spacing for a denser interface', - 'compress', - 'compact_mode', - false - ); - - content.appendChild(themeRow); - content.appendChild(compactRow); - - section.appendChild(header); - section.appendChild(content); - - return section; -} - -/** - * Privacy & Security Section - */ -function createPrivacySection() { - const section = createElement('div', { className: 'settings-section' }); - - const header = createElement('h3', { className: 'settings-section-title' }, 'Privacy & Security'); - - const content = createElement('div', { className: 'settings-section-content' }); - - // Analytics - const analyticsRow = createSettingToggle( - 'Usage Analytics', - 'Help improve TaskCraft by sharing anonymous usage data', - 'analytics', - 'usage_analytics', - true - ); - - // Data export - const exportRow = createElement('div', { className: 'setting-row' }); - const exportLabel = createElement('div', { className: 'setting-label' }); - const exportIcon = createElement('span', { className: 'material-symbols-outlined icon-md' }, 'download'); - const exportText = createElement('div', {}); - const exportTitle = createElement('div', { className: 'setting-title' }, 'Export Data'); - const exportDesc = createElement('div', { className: 'setting-description' }, 'Download all your data in JSON format'); - exportText.appendChild(exportTitle); - exportText.appendChild(exportDesc); - exportLabel.appendChild(exportIcon); - exportLabel.appendChild(exportText); - - const exportBtn = createElement('button', { - className: 'btn btn-sm btn-secondary', - onclick: () => alert('Export functionality coming soon!') - }, 'Export'); - - exportRow.appendChild(exportLabel); - exportRow.appendChild(exportBtn); - - // Delete account - const deleteRow = createElement('div', { className: 'setting-row' }); - const deleteLabel = createElement('div', { className: 'setting-label' }); - const deleteIcon = createElement('span', { - className: 'material-symbols-outlined icon-md', - style: 'color: var(--color-status-purple);' - }, 'delete_forever'); - const deleteText = createElement('div', {}); - const deleteTitle = createElement('div', { className: 'setting-title' }, 'Delete Account'); - const deleteDesc = createElement('div', { className: 'setting-description' }, 'Permanently delete your account and all data'); - deleteText.appendChild(deleteTitle); - deleteText.appendChild(deleteDesc); - deleteLabel.appendChild(deleteIcon); - deleteLabel.appendChild(deleteText); - - const deleteBtn = createElement('button', { - className: 'btn btn-sm btn-outline', - style: 'color: var(--color-status-purple); border-color: var(--color-status-purple);', - onclick: () => { - if (confirm('Are you sure you want to delete your account? This cannot be undone.')) { - alert('Account deletion is disabled in demo mode'); - } - } - }, 'Delete'); - - deleteRow.appendChild(deleteLabel); - deleteRow.appendChild(deleteBtn); - - content.appendChild(analyticsRow); - content.appendChild(exportRow); - content.appendChild(deleteRow); - - section.appendChild(header); - section.appendChild(content); - - return section; -} - -/** - * Helper: Create a setting row with label and value - */ -function createSettingRow(title, value, icon, editable = false) { - const row = createElement('div', { className: 'setting-row' }); - - const label = createElement('div', { className: 'setting-label' }); - const iconEl = createElement('span', { className: 'material-symbols-outlined icon-md' }, icon); - const textDiv = createElement('div', {}); - const titleEl = createElement('div', { className: 'setting-title' }, title); - const valueEl = createElement('div', { className: 'setting-description' }, value); - - textDiv.appendChild(titleEl); - textDiv.appendChild(valueEl); - label.appendChild(iconEl); - label.appendChild(textDiv); - - row.appendChild(label); - - if (editable) { - const editBtn = createElement('button', { - className: 'btn btn-sm btn-secondary' - }, 'Edit'); - row.appendChild(editBtn); - } - - return row; -} - -/** - * Helper: Create a toggle setting - */ -function createSettingToggle(title, description, icon, settingKey, defaultValue) { - const row = createElement('div', { className: 'setting-row' }); - - const label = createElement('div', { className: 'setting-label' }); - const iconEl = createElement('span', { className: 'material-symbols-outlined icon-md' }, icon); - const textDiv = createElement('div', {}); - const titleEl = createElement('div', { className: 'setting-title' }, title); - const descEl = createElement('div', { className: 'setting-description' }, description); - - textDiv.appendChild(titleEl); - textDiv.appendChild(descEl); - label.appendChild(iconEl); - label.appendChild(textDiv); - - // Toggle switch - const toggleContainer = createElement('label', { className: 'toggle-switch' }); - const currentValue = local.get(`setting_${settingKey}`, defaultValue); - - const checkbox = createElement('input', { - type: 'checkbox', - checked: currentValue - }); - - checkbox.addEventListener('change', (e) => { - local.set(`setting_${settingKey}`, e.target.checked); - }); - - const slider = createElement('span', { className: 'toggle-slider' }); - - toggleContainer.appendChild(checkbox); - toggleContainer.appendChild(slider); - - row.appendChild(label); - row.appendChild(toggleContainer); - - return row; -} diff --git a/frontend/js/components/theme-dropdown.js b/frontend/js/components/theme-dropdown.js deleted file mode 100644 index 7528c6c..0000000 --- a/frontend/js/components/theme-dropdown.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Theme Dropdown Component - * Dropdown menu for selecting themes - */ - -import theme from '../utils/theme.js'; -import { createElement } from '../utils/dom.js'; - -/** - * Create a theme dropdown button - * @param {HTMLElement} container - Container element - */ -export function createThemeDropdown(container) { - // Create wrapper for button + dropdown - const wrapper = createElement('div', { - style: 'position: relative;' - }); - - // Create toggle button - const button = createElement('button', { - className: 'btn btn-icon btn-secondary', - title: 'Change theme', - onclick: (e) => { - e.stopPropagation(); - toggleDropdown(); - } - }); - - const icon = createElement('span', { - className: 'material-symbols-outlined icon-lg' - }, 'palette'); - - button.appendChild(icon); - - // Create dropdown menu - const dropdown = createElement('div', { - className: 'theme-dropdown', - style: ` - position: absolute; - top: calc(100% + 0.5rem); - right: 0; - background: var(--color-surface-dark); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: 0.5rem; - min-width: 200px; - box-shadow: var(--shadow-lg); - display: none; - z-index: 1000; - ` - }); - - // Populate dropdown with themes - updateDropdownItems(dropdown); - - // Close dropdown when clicking outside - const closeOnClickOutside = (e) => { - if (!wrapper.contains(e.target)) { - dropdown.style.display = 'none'; - document.removeEventListener('click', closeOnClickOutside); - } - }; - - const toggleDropdown = () => { - const isOpen = dropdown.style.display === 'block'; - - if (isOpen) { - dropdown.style.display = 'none'; - document.removeEventListener('click', closeOnClickOutside); - } else { - dropdown.style.display = 'block'; - // Add listener on next tick to avoid immediate close - setTimeout(() => { - document.addEventListener('click', closeOnClickOutside); - }, 0); - } - }; - - // Update dropdown when theme changes - theme.subscribe(() => { - updateDropdownItems(dropdown); - }); - - wrapper.appendChild(button); - wrapper.appendChild(dropdown); - container.appendChild(wrapper); - - return wrapper; -} - -/** - * Update dropdown items with current theme state - */ -function updateDropdownItems(dropdown) { - dropdown.textContent = ''; - - const themes = theme.getAvailableThemes(); - - themes.forEach(({ id, name, isCurrent }) => { - const item = createElement('div', { - className: 'theme-dropdown-item', - style: ` - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.625rem 0.75rem; - cursor: pointer; - border-radius: var(--radius-md); - transition: background var(--transition-fast); - background: ${isCurrent ? 'var(--color-surface-highlight)' : 'transparent'}; - `, - onclick: () => { - theme.setTheme(id); - dropdown.style.display = 'none'; - } - }); - - // Color swatch - const swatch = createElement('div', { - style: ` - width: 20px; - height: 20px; - border-radius: var(--radius-sm); - background: var(--color-primary); - flex-shrink: 0; - border: 1px solid var(--color-border); - ` - }); - - // Theme name - const nameEl = createElement('div', { - style: ` - flex: 1; - font-size: var(--text-sm); - font-weight: ${isCurrent ? '600' : '500'}; - color: var(--color-text-primary); - ` - }, name); - - // Checkmark for current theme - const checkmark = createElement('span', { - className: 'material-symbols-outlined', - style: ` - font-size: 18px; - color: var(--color-primary); - opacity: ${isCurrent ? '1' : '0'}; - ` - }, 'check'); - - item.appendChild(swatch); - item.appendChild(nameEl); - item.appendChild(checkmark); - - // Hover effect - item.addEventListener('mouseenter', () => { - if (!isCurrent) { - item.style.background = 'var(--color-surface-highlight-alpha-30)'; - } - }); - - item.addEventListener('mouseleave', () => { - if (!isCurrent) { - item.style.background = 'transparent'; - } - }); - - dropdown.appendChild(item); - }); -} diff --git a/frontend/js/components/theme-picker.js b/frontend/js/components/theme-picker.js deleted file mode 100644 index 6ff6122..0000000 --- a/frontend/js/components/theme-picker.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Theme Picker Component - * UI component for selecting themes - */ - -import theme from '../utils/theme.js'; -import { createElement } from '../utils/dom.js'; - -/** - * Create a theme picker UI - * @param {HTMLElement} container - Container element to render into - * @param {Object} options - Options for the theme picker - */ -export function createThemePicker(container, options = {}) { - const { - showLabels = true, - layout = 'grid', // 'grid' or 'list' - } = options; - - // Clear container - container.textContent = ''; - - // Create theme options - const themes = theme.getAvailableThemes(); - - if (layout === 'grid') { - const grid = createElement('div', { - className: 'theme-picker-grid', - style: 'display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem;' - }); - - themes.forEach(({ id, name, isCurrent }) => { - const card = createThemeCard(id, name, isCurrent, showLabels); - grid.appendChild(card); - }); - - container.appendChild(grid); - } else { - // List layout - const list = createElement('div', { - className: 'theme-picker-list', - style: 'display: flex; flex-direction: column; gap: 0.75rem;' - }); - - themes.forEach(({ id, name, isCurrent }) => { - const item = createThemeListItem(id, name, isCurrent); - list.appendChild(item); - }); - - container.appendChild(list); - } -} - -/** - * Create a theme card (for grid layout) - */ -function createThemeCard(id, name, isCurrent, showLabels) { - const card = createElement('div', { - className: `theme-card ${isCurrent ? 'active' : ''}`, - style: ` - cursor: pointer; - border: 2px solid ${isCurrent ? 'var(--color-primary)' : 'var(--color-border)'}; - border-radius: var(--radius-lg); - padding: 1rem; - transition: all var(--transition-base); - background: var(--color-surface-dark); - `, - onclick: () => theme.setTheme(id) - }); - - // Color preview - const preview = createElement('div', { - className: 'theme-preview', - style: ` - height: 60px; - border-radius: var(--radius-md); - background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-surface-highlight) 100%); - margin-bottom: ${showLabels ? '0.75rem' : '0'}; - ` - }); - - card.appendChild(preview); - - if (showLabels) { - const label = createElement('div', { - style: ` - font-weight: 600; - color: var(--color-text-primary); - font-size: var(--text-sm); - text-align: center; - ` - }, name); - - if (isCurrent) { - const checkmark = createElement('span', { - className: 'material-symbols-outlined', - style: 'font-size: 16px; margin-left: 0.25rem; color: var(--color-primary);' - }, 'check_circle'); - label.appendChild(checkmark); - } - - card.appendChild(label); - } - - // Hover effect - card.addEventListener('mouseenter', () => { - card.style.borderColor = 'var(--color-primary)'; - card.style.transform = 'translateY(-2px)'; - }); - - card.addEventListener('mouseleave', () => { - if (!isCurrent) { - card.style.borderColor = 'var(--color-border)'; - } - card.style.transform = 'translateY(0)'; - }); - - return card; -} - -/** - * Create a theme list item (for list layout) - */ -function createThemeListItem(id, name, isCurrent) { - const item = createElement('div', { - className: `theme-list-item ${isCurrent ? 'active' : ''}`, - style: ` - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem 1rem; - cursor: pointer; - border-radius: var(--radius-lg); - background: ${isCurrent ? 'var(--color-surface-highlight)' : 'var(--color-surface-dark)'}; - border: 1px solid ${isCurrent ? 'var(--color-primary)' : 'var(--color-border)'}; - transition: all var(--transition-base); - `, - onclick: () => theme.setTheme(id) - }); - - // Color swatch - const swatch = createElement('div', { - style: ` - width: 40px; - height: 40px; - border-radius: var(--radius-md); - background: var(--color-primary); - flex-shrink: 0; - ` - }); - - // Name - const nameEl = createElement('div', { - style: ` - flex: 1; - font-weight: 500; - color: var(--color-text-primary); - ` - }, name); - - // Checkmark - const checkmark = createElement('span', { - className: 'material-symbols-outlined', - style: ` - color: var(--color-primary); - opacity: ${isCurrent ? '1' : '0'}; - transition: opacity var(--transition-base); - ` - }, 'check_circle'); - - item.appendChild(swatch); - item.appendChild(nameEl); - item.appendChild(checkmark); - - // Hover effect - item.addEventListener('mouseenter', () => { - item.style.borderColor = 'var(--color-primary)'; - }); - - item.addEventListener('mouseleave', () => { - if (!isCurrent) { - item.style.borderColor = 'var(--color-border)'; - } - }); - - return item; -} - -/** - * Create a simple theme toggle button (cycles through themes) - * @param {HTMLElement} container - Container element - */ -export function createThemeToggle(container) { - const button = createElement('button', { - className: 'btn btn-icon btn-secondary', - onclick: () => theme.nextTheme(), - title: 'Change theme' - }); - - const icon = createElement('span', { - className: 'material-symbols-outlined icon-lg' - }, 'palette'); - - button.appendChild(icon); - container.appendChild(button); - - // Update icon when theme changes - theme.subscribe(() => { - // Optional: could change icon or add animation - button.style.animation = 'none'; - setTimeout(() => { - button.style.animation = ''; - }, 10); - }); - - return button; -} diff --git a/frontend/js/fixtures/history-data.js b/frontend/js/fixtures/history-data.js deleted file mode 100644 index c68f263..0000000 --- a/frontend/js/fixtures/history-data.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Fixture data for history page - * Simulates processed events from the backend - */ - -const historyFixtures = [ - { - id: 1, - type: 'task', - status: 'success', - timestamp: new Date('2026-01-07T09:30:00').toISOString(), - inputText: 'Buy almond milk and coffee beans on the way home', - result: 'Added to Grocery List', - output: { - list: 'Grocery List', - items: ['Almond milk', 'Coffee beans'] - }, - feedback: null - }, - { - id: 2, - type: 'event', - status: 'success', - timestamp: new Date('2026-01-07T08:00:00').toISOString(), - inputText: 'Sync with dev team tomorrow at 10am about the API', - result: 'Calendar event created', - output: { - eventTitle: 'Dev Sync', - date: '2026-01-08', - time: '10:00 AM', - calendar: 'Work Calendar' - }, - feedback: 'up' - }, - { - id: 3, - type: 'reminder', - status: 'success', - timestamp: new Date('2026-01-06T15:30:00').toISOString(), - inputText: 'Cancel netflix subscription before the 15th', - result: 'Reminder set', - output: { - reminderDate: 'Oct 14th, 9:00 AM', - notification: 'enabled' - }, - feedback: null - }, - { - id: 4, - type: 'task', - status: 'failed', - timestamp: new Date('2026-01-07T07:15:00').toISOString(), - inputText: 'Schedule dentist appointment for next week sometime between 2-4pm on a Tuesday or Thursday', - result: 'Failed to process', - error: 'Unable to determine specific date and time. Please provide exact date.', - feedback: 'down' - }, - { - id: 5, - type: 'event', - status: 'success', - timestamp: new Date('2026-01-06T14:00:00').toISOString(), - inputText: 'Lunch meeting with Sarah at Olive Garden on Main Street this Friday at 1pm', - result: 'Calendar event created', - output: { - eventTitle: 'Lunch with Sarah', - location: 'Olive Garden, Main Street', - date: '2026-01-10', - time: '1:00 PM', - calendar: 'Personal Calendar' - }, - feedback: 'up' - }, - { - id: 6, - type: 'reminder', - status: 'success', - timestamp: new Date('2026-01-06T11:20:00').toISOString(), - inputText: 'Remind me to call mom tonight at 7pm', - result: 'Reminder set', - output: { - reminderDate: 'Jan 6th, 7:00 PM', - notification: 'enabled' - }, - feedback: null - }, - { - id: 7, - type: 'task', - status: 'pending', - timestamp: new Date('2026-01-07T10:45:00').toISOString(), - inputText: 'Research and compare prices for new laptops under $1000 with at least 16GB RAM', - result: 'Processing...', - feedback: null - }, - { - id: 8, - type: 'event', - status: 'failed', - timestamp: new Date('2026-01-05T16:30:00').toISOString(), - inputText: 'Team standup every morning except weekends', - result: 'Failed to process', - error: 'Recurring events require specific start date. Please specify when to begin.', - feedback: null - }, - { - id: 9, - type: 'task', - status: 'success', - timestamp: new Date('2026-01-05T13:10:00').toISOString(), - inputText: 'Pick up dry cleaning by Friday', - result: 'Added to To-Do List', - output: { - list: 'To-Do List', - deadline: 'Jan 10th' - }, - feedback: 'up' - }, - { - id: 10, - type: 'reminder', - status: 'success', - timestamp: new Date('2026-01-05T09:00:00').toISOString(), - inputText: 'Water the plants every Sunday morning', - result: 'Recurring reminder set', - output: { - recurring: 'Weekly on Sundays', - time: '9:00 AM' - }, - feedback: null - }, - { - id: 11, - type: 'event', - status: 'success', - timestamp: new Date('2026-01-04T17:45:00').toISOString(), - inputText: 'Book flight to NYC for Feb 15-20 for the design conference', - result: 'Travel task created', - output: { - type: 'Flight booking', - destination: 'NYC', - dates: 'Feb 15-20, 2026', - purpose: 'Design conference' - }, - feedback: null - }, - { - id: 12, - type: 'task', - status: 'success', - timestamp: new Date('2026-01-04T14:20:00').toISOString(), - inputText: 'Submit expense report with receipts from last week business trip', - result: 'Added to To-Do List', - output: { - list: 'Work Tasks', - priority: 'high', - deadline: 'Jan 7th' - }, - feedback: 'up' - }, - { - id: 13, - type: 'reminder', - status: 'failed', - timestamp: new Date('2026-01-04T10:30:00').toISOString(), - inputText: 'Tell me when its a good time', - result: 'Failed to process', - error: 'Cannot determine what to remind about or when. Please provide specific details.', - feedback: 'down' - }, - { - id: 14, - type: 'event', - status: 'success', - timestamp: new Date('2026-01-03T16:00:00').toISOString(), - inputText: 'Quarterly review meeting Jan 15th 3pm with entire marketing team', - result: 'Calendar event created', - output: { - eventTitle: 'Quarterly Review Meeting', - attendees: 'Marketing Team', - date: '2026-01-15', - time: '3:00 PM', - calendar: 'Work Calendar' - }, - feedback: null - }, - { - id: 15, - type: 'task', - status: 'success', - timestamp: new Date('2026-01-03T11:15:00').toISOString(), - inputText: 'Order birthday present for dad before next Monday', - result: 'Added to Shopping List', - output: { - list: 'Shopping List', - deadline: 'Jan 6th', - category: 'birthday gift' - }, - feedback: null - }, - { - id: 16, - type: 'event', - status: 'pending', - timestamp: new Date('2026-01-07T10:50:00').toISOString(), - inputText: 'Annual company retreat March 20-22 in Lake Tahoe - need to RSVP and book accommodation', - result: 'Processing...', - feedback: null - }, - { - id: 17, - type: 'task', - status: 'success', - timestamp: new Date('2026-01-02T15:40:00').toISOString(), - inputText: 'Review and approve the Q4 budget proposals from finance team', - result: 'Added to Work Tasks', - output: { - list: 'Work Tasks', - priority: 'high', - deadline: 'Jan 5th' - }, - feedback: 'up' - }, - { - id: 18, - type: 'reminder', - status: 'success', - timestamp: new Date('2026-01-02T12:00:00').toISOString(), - inputText: 'Remind me to take my vitamins every morning at 8am', - result: 'Recurring reminder set', - output: { - recurring: 'Daily', - time: '8:00 AM' - }, - feedback: null - }, - { - id: 19, - type: 'event', - status: 'success', - timestamp: new Date('2026-01-01T19:30:00').toISOString(), - inputText: 'Dinner with the Johnsons on Saturday Jan 11th at 7pm at their place', - result: 'Calendar event created', - output: { - eventTitle: 'Dinner with the Johnsons', - location: 'Their place', - date: '2026-01-11', - time: '7:00 PM', - calendar: 'Personal Calendar' - }, - feedback: 'up' - }, - { - id: 20, - type: 'task', - status: 'failed', - timestamp: new Date('2026-01-01T14:15:00').toISOString(), - inputText: 'Do that thing we talked about', - result: 'Failed to process', - error: 'Insufficient information. Cannot determine what task you are referring to.', - feedback: 'down' - }, - // Additional events for pagination testing - ...Array.from({ length: 30 }, (_, i) => { - const day = (Math.floor(i / 10) + 1).toString().padStart(2, '0'); - const hour = (10 + (i % 14)).toString().padStart(2, '0'); - const minute = (i % 60).toString().padStart(2, '0'); - return { - id: 21 + i, - type: ['task', 'event', 'reminder'][i % 3], - status: ['success', 'failed', 'pending'][i % 3], - timestamp: new Date(`2026-01-${day}T${hour}:${minute}:00`).toISOString(), - inputText: `Sample event ${21 + i}: This is a test event with some longer text to demonstrate truncation and expansion functionality. It contains enough content to require showing a "Read More" button in the UI.`, - result: i % 3 === 0 ? 'Successfully processed' : i % 3 === 1 ? 'Failed to process' : 'Processing...', - error: i % 3 === 1 ? 'Sample error message for testing' : undefined, - output: i % 3 === 0 ? { data: 'Sample output' } : undefined, - feedback: i % 4 === 0 ? 'up' : i % 4 === 1 ? 'down' : null - }; - }) -]; - -export default historyFixtures; diff --git a/frontend/js/fixtures/mock-auth.js b/frontend/js/fixtures/mock-auth.js deleted file mode 100644 index 264dd90..0000000 --- a/frontend/js/fixtures/mock-auth.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Mock Authentication Data - * Used for development testing without a backend - * - * IMPORTANT: This is for development only and should be removed when backend is ready - */ - -export const mockUsers = [ - { - id: 'demo-user-1', - email: 'demo@taskcraft.dev', - name: 'Demo User', - avatar: null, - preferences: { - theme: 'light', - notifications: true - } - } -]; - -export const mockToken = 'mock-jwt-token-' + Math.random().toString(36).substring(7); - -export const defaultMockUser = mockUsers[0]; diff --git a/frontend/js/pages/history.js b/frontend/js/pages/history.js deleted file mode 100644 index 5f9e957..0000000 --- a/frontend/js/pages/history.js +++ /dev/null @@ -1,575 +0,0 @@ -/** - * History Page Logic - * Handles event history display, filtering, pagination, and actions - */ - -import historyFixtures from '../fixtures/history-data.js'; -import { createModal } from '../components/modal.js'; -import { createPagination } from '../components/pagination.js'; -import { createMultiSelect } from '../components/multi-select.js'; -import { createElement } from '../utils/dom.js'; -import { formatRelativeTime } from '../utils/dom.js'; -import { local } from '../utils/storage.js'; - -class HistoryPage { - constructor() { - this.events = []; - this.filteredEvents = []; - this.currentPage = 1; - this.itemsPerPage = 20; - this.filters = { - dateFrom: null, - dateTo: null, - searchText: '', - types: ['task', 'event', 'reminder'], - statuses: ['success', 'failed', 'pending'] - }; - - // Load feedback from localStorage - this.feedback = local.get('event_feedback', {}); - - // Store multi-select references - this.typeSelect = null; - this.statusSelect = null; - } - - /** - * Initialize the history page - */ - async init() { - // Load events from fixtures - await this.loadEvents(); - - // Create multi-select dropdowns - this.createMultiSelects(); - - // Set up filter listeners - this.setupFilters(); - - // Initial render - this.applyFilters(); - this.renderList(); - this.renderPagination(); - } - - /** - * Create multi-select dropdown components - */ - createMultiSelects() { - // Type filter - const typeContainer = document.getElementById('type-filter-container'); - if (typeContainer) { - this.typeSelect = createMultiSelect({ - id: 'type-multi-select', - label: 'Types', - options: [ - { value: 'task', label: 'Task', color: 'task' }, - { value: 'event', label: 'Event', color: 'event' }, - { value: 'reminder', label: 'Reminder', color: 'reminder' } - ], - defaultSelected: ['task', 'event', 'reminder'], - onChange: (selected) => { - this.filters.types = selected; - this.applyFiltersAndRender(); - } - }); - typeContainer.appendChild(this.typeSelect); - } - - // Status filter - const statusContainer = document.getElementById('status-filter-container'); - if (statusContainer) { - this.statusSelect = createMultiSelect({ - id: 'status-multi-select', - label: 'Statuses', - options: [ - { value: 'success', label: 'Success' }, - { value: 'failed', label: 'Failed' }, - { value: 'pending', label: 'Pending' } - ], - defaultSelected: ['success', 'failed', 'pending'], - onChange: (selected) => { - this.filters.statuses = selected; - this.applyFiltersAndRender(); - } - }); - statusContainer.appendChild(this.statusSelect); - } - } - - /** - * Load events from fixtures (simulates API call) - */ - async loadEvents() { - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, 100)); - - // Clone fixtures and merge with saved feedback - this.events = historyFixtures.map(event => ({ - ...event, - feedback: this.feedback[event.id] || event.feedback - })); - - // Sort by timestamp (newest first) - this.events.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); - } - - /** - * Set up filter event listeners - */ - setupFilters() { - // Date filters - const dateFrom = document.getElementById('filter-date-from'); - const dateTo = document.getElementById('filter-date-to'); - - if (dateFrom) { - dateFrom.addEventListener('change', () => { - this.filters.dateFrom = dateFrom.value ? new Date(dateFrom.value) : null; - this.applyFiltersAndRender(); - }); - } - - if (dateTo) { - dateTo.addEventListener('change', () => { - this.filters.dateTo = dateTo.value ? new Date(dateTo.value + 'T23:59:59') : null; - this.applyFiltersAndRender(); - }); - } - - // Text search - const searchInput = document.getElementById('filter-text'); - if (searchInput) { - searchInput.addEventListener('input', () => { - this.filters.searchText = searchInput.value.toLowerCase(); - this.applyFiltersAndRender(); - }); - } - - // Reset filters button - const resetBtn = document.getElementById('reset-filters'); - if (resetBtn) { - resetBtn.addEventListener('click', () => { - this.resetFilters(); - }); - } - } - - /** - * Reset all filters to default - */ - resetFilters() { - // Reset filter state - this.filters = { - dateFrom: null, - dateTo: null, - searchText: '', - types: ['task', 'event', 'reminder'], - statuses: ['success', 'failed', 'pending'] - }; - - // Reset UI - const dateFrom = document.getElementById('filter-date-from'); - const dateTo = document.getElementById('filter-date-to'); - const searchInput = document.getElementById('filter-text'); - - if (dateFrom) dateFrom.value = ''; - if (dateTo) dateTo.value = ''; - if (searchInput) searchInput.value = ''; - - // Reset multi-select dropdowns - if (this.typeSelect && this.typeSelect.setSelected) { - this.typeSelect.setSelected(['task', 'event', 'reminder']); - } - - if (this.statusSelect && this.statusSelect.setSelected) { - this.statusSelect.setSelected(['success', 'failed', 'pending']); - } - - this.applyFiltersAndRender(); - } - - /** - * Apply filters to events - */ - applyFilters() { - this.filteredEvents = this.events.filter(event => { - const eventDate = new Date(event.timestamp); - - // Date range filter - if (this.filters.dateFrom && eventDate < this.filters.dateFrom) return false; - if (this.filters.dateTo && eventDate > this.filters.dateTo) return false; - - // Text search (search in input text) - if (this.filters.searchText && !event.inputText.toLowerCase().includes(this.filters.searchText)) { - return false; - } - - // Type filter - if (!this.filters.types.includes(event.type)) return false; - - // Status filter - if (!this.filters.statuses.includes(event.status)) return false; - - return true; - }); - - this.currentPage = 1; // Reset to first page when filters change - } - - /** - * Apply filters and re-render - */ - applyFiltersAndRender() { - this.applyFilters(); - this.renderList(); - this.renderPagination(); - } - - /** - * Render the event list for current page - */ - renderList() { - const start = (this.currentPage - 1) * this.itemsPerPage; - const end = start + this.itemsPerPage; - const pageEvents = this.filteredEvents.slice(start, end); - - const container = document.getElementById('history-list'); - if (!container) return; - - container.textContent = ''; - - if (pageEvents.length === 0) { - // Empty state - const emptyState = createElement('div', { - style: 'text-align: center; padding: 3rem; color: var(--color-text-secondary);' - }); - - const icon = createElement('span', { - className: 'material-symbols-outlined', - style: 'font-size: 4rem; opacity: 0.3;' - }, 'search_off'); - - const text = createElement('p', { - style: 'margin-top: 1rem; font-size: var(--text-lg);' - }, 'No events found matching your filters'); - - emptyState.appendChild(icon); - emptyState.appendChild(text); - container.appendChild(emptyState); - - // Update count - document.getElementById('result-count').textContent = 'No events found'; - return; - } - - pageEvents.forEach(event => { - const item = this.createEventItem(event); - container.appendChild(item); - }); - - // Update count - const resultCount = document.getElementById('result-count'); - if (resultCount) { - resultCount.textContent = `Showing ${start + 1}-${Math.min(end, this.filteredEvents.length)} of ${this.filteredEvents.length} events`; - } - } - - /** - * Create an event list item - */ - createEventItem(event) { - const item = createElement('div', { - className: 'list-item event-item', - 'data-status': event.status, - 'data-type': event.type - }); - - // Header with badges and details button - const header = createElement('div', { - className: 'list-item-header' - }); - - const meta = createElement('div', { - className: 'list-item-meta' - }); - - // Type badge - const typeBadge = createElement('span', { - className: `badge badge-${event.type}` - }, event.type.charAt(0).toUpperCase() + event.type.slice(1)); - - // Status badge - const statusBadge = createElement('span', { - className: `status-badge-${event.status}` - }, event.status); - - // Timestamp - const timestamp = createElement('span', { - className: 'timestamp' - }, formatRelativeTime(event.timestamp)); - - meta.appendChild(typeBadge); - meta.appendChild(statusBadge); - meta.appendChild(timestamp); - - // Details button - const detailsBtn = createElement('button', { - className: 'btn-icon-sm', - title: 'View details', - onclick: () => this.openEventDetails(event) - }); - - const detailsIcon = createElement('span', { - className: 'material-symbols-outlined icon-md' - }, 'open_in_full'); - - detailsBtn.appendChild(detailsIcon); - - header.appendChild(meta); - header.appendChild(detailsBtn); - - // Event text (truncated) - const text = createElement('p', { - className: 'list-item-text truncated' - }, `"${event.inputText}"`); - - // Divider - const divider = createElement('div', { - className: 'divider' - }); - - // Footer with result and actions - const footer = createElement('div', { - className: 'list-item-footer' - }); - - // Result - const resultDiv = createElement('div', { - className: 'event-result' - }); - - const resultIcon = createElement('span', { - className: 'material-symbols-outlined icon-sm' - }, event.status === 'success' ? 'check_circle' : event.status === 'failed' ? 'error' : 'pending'); - - const resultText = createElement('span', {}, event.result); - - resultDiv.appendChild(resultIcon); - resultDiv.appendChild(resultText); - - // Actions - const actions = createElement('div', { - className: 'event-actions' - }); - - // Retry button (only for failed events) - if (event.status === 'failed') { - const retryBtn = createElement('button', { - className: 'btn btn-xs btn-secondary retry-btn', - onclick: () => this.retryEvent(event.id) - }); - - const retryIcon = createElement('span', { - className: 'material-symbols-outlined icon-sm' - }, 'refresh'); - - const retryText = createElement('span', {}, 'Retry'); - - retryBtn.appendChild(retryIcon); - retryBtn.appendChild(retryText); - - actions.appendChild(retryBtn); - } - - // Feedback buttons - const thumbsUpBtn = createElement('button', { - className: `btn-icon-sm feedback-btn ${event.feedback === 'up' ? 'active up' : ''}`, - 'data-feedback': 'up', - title: 'Helpful', - onclick: () => this.submitFeedback(event.id, 'up') - }); - - const thumbsUpIcon = createElement('span', { - className: 'material-symbols-outlined icon-sm' - }, 'thumb_up'); - - thumbsUpBtn.appendChild(thumbsUpIcon); - - const thumbsDownBtn = createElement('button', { - className: `btn-icon-sm feedback-btn ${event.feedback === 'down' ? 'active down' : ''}`, - 'data-feedback': 'down', - title: 'Not helpful', - onclick: () => this.submitFeedback(event.id, 'down') - }); - - const thumbsDownIcon = createElement('span', { - className: 'material-symbols-outlined icon-sm' - }, 'thumb_down'); - - thumbsDownBtn.appendChild(thumbsDownIcon); - - actions.appendChild(thumbsUpBtn); - actions.appendChild(thumbsDownBtn); - - footer.appendChild(resultDiv); - footer.appendChild(actions); - - // Assemble item - item.appendChild(header); - item.appendChild(text); - item.appendChild(divider); - item.appendChild(footer); - - return item; - } - - /** - * Open modal with event details - */ - openEventDetails(event) { - // Build detailed content - const content = createElement('div', { - className: 'event-details' - }); - - // Input section - const inputHeading = createElement('h3', {}, 'Input'); - const inputText = createElement('p', {}, event.inputText); - content.appendChild(inputHeading); - content.appendChild(inputText); - - // Result section - const resultHeading = createElement('h3', {}, 'Result'); - const resultText = createElement('p', {}, event.result); - content.appendChild(resultHeading); - content.appendChild(resultText); - - // Output data (if exists) - if (event.output) { - const outputHeading = createElement('h3', {}, 'Details'); - const outputPre = createElement('pre', { - style: 'background: var(--color-surface-highlight); padding: 1rem; border-radius: var(--radius-md); overflow-x: auto;' - }, JSON.stringify(event.output, null, 2)); - content.appendChild(outputHeading); - content.appendChild(outputPre); - } - - // Error (if failed) - if (event.error) { - const errorHeading = createElement('h3', {}, 'Error'); - const errorText = createElement('p', { - style: 'color: var(--color-status-purple);' - }, event.error); - content.appendChild(errorHeading); - content.appendChild(errorText); - } - - // Status and timestamp - const statusHeading = createElement('h3', {}, 'Status & Time'); - const statusText = createElement('p', {}, - `Status: ${event.status} | Processed: ${new Date(event.timestamp).toLocaleString()}` - ); - content.appendChild(statusHeading); - content.appendChild(statusText); - - // Create and open modal - const modal = createModal({ - title: `${event.type.charAt(0).toUpperCase() + event.type.slice(1)} Event Details`, - content: content, - size: 'medium' - }); - - modal.open(); - } - - /** - * Retry a failed event - */ - async retryEvent(eventId) { - // Find the event - const eventIndex = this.events.findIndex(e => e.id === eventId); - if (eventIndex === -1) return; - - // Simulate retry (in real app, would call API) - const event = this.events[eventIndex]; - - // Update status to pending - event.status = 'pending'; - event.result = 'Retrying...'; - - // Re-render - this.renderList(); - - // Simulate processing - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Randomly succeed or fail - const success = Math.random() > 0.3; - - if (success) { - event.status = 'success'; - event.result = 'Successfully processed on retry'; - event.error = undefined; - } else { - event.status = 'failed'; - event.result = 'Failed to process'; - event.error = 'Retry failed. Please check the input and try again.'; - } - - // Re-render - this.applyFilters(); - this.renderList(); - } - - /** - * Submit feedback (thumbs up/down) - */ - submitFeedback(eventId, feedbackType) { - // Find the event - const event = this.events.find(e => e.id === eventId); - if (!event) return; - - // Toggle feedback (click same button to remove) - if (event.feedback === feedbackType) { - event.feedback = null; - delete this.feedback[eventId]; - } else { - event.feedback = feedbackType; - this.feedback[eventId] = feedbackType; - } - - // Save to localStorage - local.set('event_feedback', this.feedback); - - // Re-render - this.renderList(); - } - - /** - * Render pagination controls - */ - renderPagination() { - const container = document.getElementById('pagination-container'); - if (!container) return; - - createPagination(container, { - totalItems: this.filteredEvents.length, - itemsPerPage: this.itemsPerPage, - currentPage: this.currentPage, - onPageChange: (page) => { - this.currentPage = page; - this.renderList(); - this.renderPagination(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }); - } -} - -// Export function to initialize history page -export async function initHistoryPage() { - const historyPage = new HistoryPage(); - await historyPage.init(); -} - -export default HistoryPage; diff --git a/frontend/js/pages/login.js b/frontend/js/pages/login.js deleted file mode 100644 index 110a9f4..0000000 --- a/frontend/js/pages/login.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Login Page Logic - * Handles user authentication and mock login for development - */ - -import auth from '../auth.js'; -import api from '../api.js'; -import router from '../router.js'; -import { createElement } from '../utils/dom.js'; - -export function initLoginPage() { - // Show demo login button only in dev environments - const mockSection = document.getElementById('mock-auth-section'); - if (mockSection && auth.constructor.isMockEnvironment()) { - mockSection.style.display = 'block'; - } - - // Handle demo login - const demoBtn = document.getElementById('demo-login-btn'); - if (demoBtn) { - demoBtn.addEventListener('click', async () => { - demoBtn.disabled = true; - demoBtn.textContent = 'Logging in...'; - - try { - await auth.loginAsMock(); - // AuthManager will trigger navigation via listener - } catch (error) { - console.error('Demo login failed:', error); - alert('Demo login failed'); - - // Restore button content safely using createElement - demoBtn.textContent = ''; - demoBtn.disabled = false; - - const icon = createElement('span', { - className: 'material-symbols-outlined icon-md' - }, 'science'); - const text = createElement('span', {}, 'Continue as Demo User'); - - demoBtn.appendChild(icon); - demoBtn.appendChild(text); - } - }); - } - - // Handle regular login form - const form = document.getElementById('login-form'); - if (form) { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - - try { - await auth.login(email, password); - // Will navigate on success via auth listener - } catch (error) { - console.error('Login failed:', error); - alert('Login failed. Please check your credentials.'); - } - }); - } -} diff --git a/frontend/js/router.js b/frontend/js/router.js deleted file mode 100644 index 7581ae8..0000000 --- a/frontend/js/router.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Simple SPA Router using History API - * - * Security Note: This router loads trusted page templates from our own codebase. - * It uses template elements to safely parse HTML. Do NOT pass user-generated content - * directly to route handlers without sanitization. - */ - -class Router { - constructor() { - this.routes = {}; - this.currentRoute = null; - this.beforeNavigateHooks = []; - this.afterNavigateHooks = []; - - // Handle browser back/forward buttons - window.addEventListener('popstate', () => { - this.handleRoute(window.location.pathname); - }); - - // Intercept link clicks - document.addEventListener('click', (e) => { - if (e.target.matches('[data-link]')) { - e.preventDefault(); - this.navigate(e.target.getAttribute('href')); - } - }); - } - - /** - * Register a route - * @param {string} path - Route path (e.g., '/dashboard', '/settings') - * @param {Function} handler - Function that returns HTML content or loads page - */ - addRoute(path, handler) { - this.routes[path] = handler; - } - - /** - * Register multiple routes at once - * @param {Object} routes - Object with path: handler pairs - */ - addRoutes(routes) { - Object.entries(routes).forEach(([path, handler]) => { - this.addRoute(path, handler); - }); - } - - /** - * Navigate to a route - * @param {string} path - Path to navigate to - */ - async navigate(path) { - // Run before-navigate hooks - for (const hook of this.beforeNavigateHooks) { - const result = await hook(path); - if (result === false) return; // Hook cancelled navigation - } - - window.history.pushState(null, null, path); - await this.handleRoute(path); - - // Run after-navigate hooks - for (const hook of this.afterNavigateHooks) { - await hook(path); - } - } - - /** - * Handle route rendering - * @param {string} path - Path to render - */ - async handleRoute(path) { - // Normalize path - let normalizedPath = path; - - // Handle index.html or empty path - if (path === '/index.html' || path === '' || path === '/frontend/' || path === '/frontend/index.html') { - normalizedPath = '/'; - } - - const route = this.routes[normalizedPath] || this.routes['/404']; - - if (!route) { - console.error(`No route found for ${normalizedPath}`); - // Fallback to home if no 404 route exists - if (this.routes['/']) { - await this.routes['/'](); - } - return; - } - - this.currentRoute = normalizedPath; - - try { - const content = await route(); - const appContainer = document.getElementById('app'); - - if (appContainer) { - // Clear existing content - appContainer.textContent = ''; - - if (typeof content === 'string') { - // Use template element to safely parse trusted HTML from our page files - const template = document.createElement('template'); - template.innerHTML = content; - appContainer.appendChild(template.content); - } else if (content instanceof HTMLElement) { - appContainer.appendChild(content); - } - } - } catch (error) { - console.error('Error rendering route:', error); - if (this.routes['/error']) { - const errorContent = await this.routes['/error'](error); - const appContainer = document.getElementById('app'); - appContainer.textContent = ''; - const template = document.createElement('template'); - template.innerHTML = errorContent; - appContainer.appendChild(template.content); - } - } - } - - /** - * Add a hook that runs before navigation - * @param {Function} hook - Hook function (can return false to cancel navigation) - */ - beforeNavigate(hook) { - this.beforeNavigateHooks.push(hook); - } - - /** - * Add a hook that runs after navigation - * @param {Function} hook - Hook function - */ - afterNavigate(hook) { - this.afterNavigateHooks.push(hook); - } - - /** - * Start the router - */ - start() { - this.handleRoute(window.location.pathname); - } - - /** - * Get current route path - */ - getCurrentPath() { - return this.currentRoute; - } - - /** - * Get query parameters from URL - * @returns {Object} Query parameters as key-value pairs - */ - getQueryParams() { - const params = new URLSearchParams(window.location.search); - const result = {}; - for (const [key, value] of params) { - result[key] = value; - } - return result; - } -} - -// Create singleton instance -const router = new Router(); - -export default router; diff --git a/frontend/js/utils/dom.js b/frontend/js/utils/dom.js deleted file mode 100644 index c0a66d1..0000000 --- a/frontend/js/utils/dom.js +++ /dev/null @@ -1,224 +0,0 @@ -/** - * DOM Utility Functions - */ - -/** - * Create an element with attributes and children - * @param {string} tag - HTML tag name - * @param {Object} attrs - Attributes object - * @param {Array|string|HTMLElement} children - Children elements or text - */ -export function createElement(tag, attrs = {}, children = []) { - const element = document.createElement(tag); - - // Set attributes - Object.entries(attrs).forEach(([key, value]) => { - if (key === 'className') { - element.className = value; - } else if (key === 'dataset') { - Object.entries(value).forEach(([dataKey, dataValue]) => { - element.dataset[dataKey] = dataValue; - }); - } else if (key.startsWith('on') && typeof value === 'function') { - const eventName = key.substring(2).toLowerCase(); - element.addEventListener(eventName, value); - } else { - element.setAttribute(key, value); - } - }); - - // Add children - const childArray = Array.isArray(children) ? children : [children]; - childArray.forEach((child) => { - if (typeof child === 'string') { - element.appendChild(document.createTextNode(child)); - } else if (child instanceof HTMLElement) { - element.appendChild(child); - } - }); - - return element; -} - -/** - * Query selector wrapper with error handling - * @param {string} selector - CSS selector - * @param {HTMLElement} parent - Parent element (default: document) - */ -export function $(selector, parent = document) { - return parent.querySelector(selector); -} - -/** - * Query selector all wrapper - * @param {string} selector - CSS selector - * @param {HTMLElement} parent - Parent element (default: document) - */ -export function $$(selector, parent = document) { - return Array.from(parent.querySelectorAll(selector)); -} - -/** - * Add event listener with cleanup - * @param {HTMLElement} element - Target element - * @param {string} event - Event name - * @param {Function} handler - Event handler - * @param {Object} options - Event listener options - * @returns {Function} Cleanup function - */ -export function on(element, event, handler, options = {}) { - element.addEventListener(event, handler, options); - - return () => { - element.removeEventListener(event, handler, options); - }; -} - -/** - * Add delegated event listener - * @param {HTMLElement} parent - Parent element - * @param {string} selector - CSS selector for target elements - * @param {string} event - Event name - * @param {Function} handler - Event handler - */ -export function delegate(parent, selector, event, handler) { - const wrappedHandler = (e) => { - const target = e.target.closest(selector); - if (target && parent.contains(target)) { - handler.call(target, e); - } - }; - - parent.addEventListener(event, wrappedHandler); - - return () => { - parent.removeEventListener(event, wrappedHandler); - }; -} - -/** - * Toggle class on element - * @param {HTMLElement} element - Target element - * @param {string} className - Class name - * @param {boolean} force - Force add (true) or remove (false) - */ -export function toggleClass(element, className, force) { - element.classList.toggle(className, force); -} - -/** - * Show element - * @param {HTMLElement} element - Target element - */ -export function show(element) { - element.style.display = ''; - element.removeAttribute('hidden'); -} - -/** - * Hide element - * @param {HTMLElement} element - Target element - */ -export function hide(element) { - element.style.display = 'none'; -} - -/** - * Remove all children from element - * @param {HTMLElement} element - Target element - */ -export function empty(element) { - element.textContent = ''; -} - -/** - * Load HTML from file - * @param {string} url - URL to HTML file - */ -export async function loadHTML(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to load ${url}`); - } - return await response.text(); -} - -/** - * Debounce function - * @param {Function} func - Function to debounce - * @param {number} wait - Wait time in milliseconds - */ -export function debounce(func, wait = 300) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -/** - * Throttle function - * @param {Function} func - Function to throttle - * @param {number} limit - Time limit in milliseconds - */ -export function throttle(func, limit = 300) { - let inThrottle; - return function executedFunction(...args) { - if (!inThrottle) { - func(...args); - inThrottle = true; - setTimeout(() => (inThrottle = false), limit); - } - }; -} - -/** - * Escape HTML to prevent XSS - * @param {string} html - HTML string - */ -export function escapeHTML(html) { - const div = document.createElement('div'); - div.textContent = html; - return div.innerHTML; -} - -/** - * Format date for display - * @param {Date|string} date - Date object or string - * @param {Object} options - Intl.DateTimeFormat options - */ -export function formatDate(date, options = {}) { - const dateObj = date instanceof Date ? date : new Date(date); - const defaultOptions = { - year: 'numeric', - month: 'short', - day: 'numeric', - ...options, - }; - return new Intl.DateTimeFormat('en-US', defaultOptions).format(dateObj); -} - -/** - * Format relative time (e.g., "2 mins ago") - * @param {Date|string} date - Date object or string - */ -export function formatRelativeTime(date) { - const dateObj = date instanceof Date ? date : new Date(date); - const now = new Date(); - const diffMs = now - dateObj; - const diffSec = Math.floor(diffMs / 1000); - const diffMin = Math.floor(diffSec / 60); - const diffHour = Math.floor(diffMin / 60); - const diffDay = Math.floor(diffHour / 24); - - if (diffSec < 60) return 'just now'; - if (diffMin < 60) return `${diffMin} min${diffMin > 1 ? 's' : ''} ago`; - if (diffHour < 24) return `${diffHour} hour${diffHour > 1 ? 's' : ''} ago`; - if (diffDay < 7) return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`; - - return formatDate(dateObj); -} diff --git a/frontend/js/utils/storage.js b/frontend/js/utils/storage.js deleted file mode 100644 index 8a1b094..0000000 --- a/frontend/js/utils/storage.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Storage Utility Functions - * Wrapper for localStorage and sessionStorage with error handling and JSON support - */ - -class StorageManager { - constructor(storage = localStorage) { - this.storage = storage; - } - - /** - * Get item from storage - * @param {string} key - Storage key - * @param {any} defaultValue - Default value if key doesn't exist - */ - get(key, defaultValue = null) { - try { - const item = this.storage.getItem(key); - - if (item === null) { - return defaultValue; - } - - // Try to parse as JSON - try { - return JSON.parse(item); - } catch { - // Return as string if not JSON - return item; - } - } catch (error) { - console.error(`Failed to get item "${key}" from storage:`, error); - return defaultValue; - } - } - - /** - * Set item in storage - * @param {string} key - Storage key - * @param {any} value - Value to store (will be JSON stringified) - */ - set(key, value) { - try { - const serialized = typeof value === 'string' ? value : JSON.stringify(value); - this.storage.setItem(key, serialized); - return true; - } catch (error) { - console.error(`Failed to set item "${key}" in storage:`, error); - return false; - } - } - - /** - * Remove item from storage - * @param {string} key - Storage key - */ - remove(key) { - try { - this.storage.removeItem(key); - return true; - } catch (error) { - console.error(`Failed to remove item "${key}" from storage:`, error); - return false; - } - } - - /** - * Clear all items from storage - */ - clear() { - try { - this.storage.clear(); - return true; - } catch (error) { - console.error('Failed to clear storage:', error); - return false; - } - } - - /** - * Check if key exists in storage - * @param {string} key - Storage key - */ - has(key) { - return this.storage.getItem(key) !== null; - } - - /** - * Get all keys from storage - */ - keys() { - return Object.keys(this.storage); - } - - /** - * Get number of items in storage - */ - size() { - return this.storage.length; - } - - /** - * Set item with expiration time - * @param {string} key - Storage key - * @param {any} value - Value to store - * @param {number} ttl - Time to live in milliseconds - */ - setWithExpiry(key, value, ttl) { - const item = { - value, - expiry: Date.now() + ttl, - }; - return this.set(key, item); - } - - /** - * Get item with expiration check - * @param {string} key - Storage key - * @param {any} defaultValue - Default value if expired or doesn't exist - */ - getWithExpiry(key, defaultValue = null) { - const item = this.get(key); - - if (!item) { - return defaultValue; - } - - if (item.expiry && Date.now() > item.expiry) { - this.remove(key); - return defaultValue; - } - - return item.value !== undefined ? item.value : defaultValue; - } - - /** - * Update existing item (merge for objects) - * @param {string} key - Storage key - * @param {any} updates - Updates to merge - */ - update(key, updates) { - const existing = this.get(key); - - if (typeof existing === 'object' && typeof updates === 'object') { - return this.set(key, { ...existing, ...updates }); - } - - return this.set(key, updates); - } -} - -// Create instances for localStorage and sessionStorage -const local = new StorageManager(localStorage); -const session = new StorageManager(sessionStorage); - -export { local, session, StorageManager }; -export default local; diff --git a/frontend/js/utils/theme.js b/frontend/js/utils/theme.js deleted file mode 100644 index 227d44b..0000000 --- a/frontend/js/utils/theme.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Theme Management Utility - * Handles dynamic theme switching and persistence - */ - -import { local } from './storage.js'; - -const THEME_STORAGE_KEY = 'app_theme'; -const DEFAULT_THEME = 'default'; // No data attribute = default theme - -class ThemeManager { - constructor() { - this.currentTheme = DEFAULT_THEME; - this.listeners = []; - - // Available themes (dark themes first, then light themes) - this.themes = { - default: 'Golden Hour', - ocean: 'Ocean Blue', - forest: 'Forest Green', - midnight: 'Midnight Purple', - sunset: 'Sunset Orange', - rose: 'Rose Pink', - daylight: 'Daylight', - cloud: 'Cloud', - meadow: 'Meadow', - }; - - // Load saved theme - this.loadTheme(); - } - - /** - * Get list of available themes - */ - getAvailableThemes() { - return Object.entries(this.themes).map(([id, name]) => ({ - id, - name, - isCurrent: id === this.currentTheme, - })); - } - - /** - * Load saved theme from storage - */ - loadTheme() { - const savedTheme = local.get(THEME_STORAGE_KEY, DEFAULT_THEME); - this.setTheme(savedTheme, false); // Don't notify on initial load - } - - /** - * Set the current theme - * @param {string} themeId - Theme identifier - * @param {boolean} notify - Whether to notify listeners - */ - setTheme(themeId, notify = true) { - // Validate theme exists - if (!this.themes[themeId]) { - console.warn(`Theme "${themeId}" not found, falling back to default`); - themeId = DEFAULT_THEME; - } - - this.currentTheme = themeId; - - // Apply theme to document - if (themeId === DEFAULT_THEME) { - document.documentElement.removeAttribute('data-theme'); - } else { - document.documentElement.setAttribute('data-theme', themeId); - } - - // Save to storage - local.set(THEME_STORAGE_KEY, themeId); - - // Notify listeners - if (notify) { - this.notifyListeners(themeId); - } - } - - /** - * Get current theme - */ - getCurrentTheme() { - return { - id: this.currentTheme, - name: this.themes[this.currentTheme], - }; - } - - /** - * Subscribe to theme changes - * @param {Function} listener - Callback function (themeId) => void - */ - subscribe(listener) { - this.listeners.push(listener); - - // Return unsubscribe function - return () => { - this.listeners = this.listeners.filter(l => l !== listener); - }; - } - - /** - * Notify all listeners of theme change - * @param {string} themeId - New theme ID - */ - notifyListeners(themeId) { - this.listeners.forEach(listener => { - try { - listener(themeId); - } catch (error) { - console.error('Theme listener error:', error); - } - }); - } - - /** - * Cycle to next theme (useful for theme toggle button) - */ - nextTheme() { - const themeIds = Object.keys(this.themes); - const currentIndex = themeIds.indexOf(this.currentTheme); - const nextIndex = (currentIndex + 1) % themeIds.length; - this.setTheme(themeIds[nextIndex]); - } -} - -// Create singleton instance -const theme = new ThemeManager(); - -export default theme; -export { ThemeManager }; diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index e41f297..7377936 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -1,37 +1,96 @@ -import { createSignal } from 'solid-js'; +import { createSignal, createEffect, onCleanup } from 'solid-js'; import Modal from './Modal'; +import { api } from '../lib/api'; interface SettingsModalProps { isOpen: boolean; onClose: () => void; } +interface Settings { + ai_context: string; + auto_sync: boolean; + usage_analytics: boolean; +} + export default function SettingsModal(props: SettingsModalProps) { - const [enableNotifications, setEnableNotifications] = createSignal( - localStorage.getItem('setting_enable_notifications') === 'true' - ); - const [emailNotifications, setEmailNotifications] = createSignal( - localStorage.getItem('setting_email_notifications') === 'true' - ); - const [notificationSound, setNotificationSound] = createSignal( - localStorage.getItem('setting_notification_sound') !== 'false' - ); const [autoSync, setAutoSync] = createSignal( localStorage.getItem('setting_auto_sync') !== 'false' ); - const [compactMode, setCompactMode] = createSignal( - localStorage.getItem('setting_compact_mode') === 'true' - ); const [usageAnalytics, setUsageAnalytics] = createSignal( localStorage.getItem('setting_usage_analytics') !== 'false' ); + const [aiContext, setAiContext] = createSignal( + localStorage.getItem('setting_ai_context') || '' + ); + const [isLoading, setIsLoading] = createSignal(false); + + // Fetch settings from backend when modal opens + createEffect(() => { + if (props.isOpen) { + fetchSettings(); + } + }); + + const fetchSettings = async () => { + try { + setIsLoading(true); + const settings: Settings = await api.get('/settings'); + + setAiContext(settings.ai_context || ''); + setAutoSync(settings.auto_sync); + setUsageAnalytics(settings.usage_analytics); + + // Update localStorage cache + localStorage.setItem('setting_ai_context', settings.ai_context || ''); + localStorage.setItem('setting_auto_sync', String(settings.auto_sync)); + localStorage.setItem('setting_usage_analytics', String(settings.usage_analytics)); + } catch (error) { + console.error('Failed to fetch settings:', error); + // Keep using localStorage values on error + } finally { + setIsLoading(false); + } + }; + + const saveSettings = async (updates: Partial) => { + try { + await api.patch('/settings', updates); + console.log('Settings saved successfully'); + } catch (error) { + console.error('Failed to save settings:', error); + } + }; const handleToggle = (key: string, setter: (value: boolean) => void) => (e: InputEvent) => { const checked = (e.currentTarget as HTMLInputElement).checked; setter(checked); localStorage.setItem(`setting_${key}`, String(checked)); + + // Save to backend + const updates: Partial = {}; + if (key === 'auto_sync') updates.auto_sync = checked; + if (key === 'usage_analytics') updates.usage_analytics = checked; + saveSettings(updates); + }; + + let textareaDebounceTimer: number | undefined; + const handleAiContextChange = (e: InputEvent) => { + const value = (e.currentTarget as HTMLTextAreaElement).value; + setAiContext(value); + localStorage.setItem('setting_ai_context', value); + + // Debounce API call + clearTimeout(textareaDebounceTimer); + textareaDebounceTimer = setTimeout(() => { + saveSettings({ ai_context: value }); + }, 1000) as unknown as number; }; + onCleanup(() => { + clearTimeout(textareaDebounceTimer); + }); + return (
@@ -59,75 +118,28 @@ export default function SettingsModal(props: SettingsModalProps) {
-
-
- lock -
-
Password
-
Change your account password
-
-
- -
- {/* Notifications Section */} + {/* AI Assistant Section */}
-

Notifications

+

AI Assistant

-
+
- notifications + psychology
-
Enable Notifications
-
Receive notifications for upcoming events and reminders
+
Custom Context
+
Provide additional context to help the AI assistant understand your preferences and needs
- -
- -
-
- email -
-
Email Notifications
-
Get email summaries of your daily tasks
-
-
- -
- -
-
- volume_up -
-
Notification Sound
-
Play a sound when you receive notifications
-
-
- +