/** * Projects Landing Page JavaScript * CodeNomad-style: Shows projects, clicking opens session picker */ // State let projects = []; let isLoading = false; // Load on page load document.addEventListener('DOMContentLoaded', () => { checkAuth(); initializeHero(); loadProjects(); initializeKeyboardShortcuts(); }); // Check authentication async function checkAuth() { try { const res = await fetch('/claude/api/auth/status'); if (!res.ok) { throw new Error('Request failed'); } const data = await res.json(); if (!data.authenticated) { // Show login modal instead of redirecting showLoginModal(); return; } // Update nav with username if (data.username) { document.querySelector('.nav-logo').textContent = `Claude Code (${data.username})`; } } catch (error) { console.error('[checkAuth] Error:', error); showLoginModal(); } } // Show login modal function showLoginModal() { const modal = document.getElementById('login-modal'); if (modal) { modal.style.display = 'flex'; } } // Close login modal function closeLoginModal() { const modal = document.getElementById('login-modal'); if (modal) { modal.style.display = 'none'; } } // Handle login form submission async function handleLogin(event) { event.preventDefault(); const username = document.getElementById('login-username').value; const password = document.getElementById('login-password').value; const errorDiv = document.getElementById('login-error'); try { const res = await fetch('/claude/api/login', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await res.json(); if (data.success) { // Login successful closeLoginModal(); loadProjects(); // Update nav with username if (data.username) { document.querySelector('.nav-logo').textContent = `Claude Code (${data.username})`; } } else { // Show error errorDiv.textContent = data.error || 'Login failed'; errorDiv.style.display = 'block'; } } catch (error) { console.error('[handleLogin] Error:', error); errorDiv.textContent = 'Login failed. Please try again.'; errorDiv.style.display = 'block'; } } /** * Initialize hero section */ function initializeHero() { const selectFolderBtn = document.getElementById('select-folder-btn'); if (selectFolderBtn) { selectFolderBtn.addEventListener('click', async () => { await showFolderExplorer(); }); } } /** * Show folder explorer modal */ async function showFolderExplorer() { try { // Load the folder explorer modal if not already loaded if (!window.FolderExplorer) { await loadScript('/claude/claude-ide/components/folder-explorer-modal.js'); } // Show folder explorer if (window.FolderExplorer) { window.FolderExplorer.show(); } } catch (error) { console.error('Error showing folder explorer:', error); showToast('Failed to open folder explorer', 'error'); } } /** * Load all projects from server */ async function loadProjects() { const grid = document.getElementById('projects-grid'); const empty = document.getElementById('projects-empty'); const loading = document.getElementById('projects-loading'); const error = document.getElementById('projects-error'); if (grid) grid.style.display = 'none'; if (empty) empty.style.display = 'none'; if (error) error.style.display = 'none'; if (loading) loading.style.display = 'block'; try { console.log('[Projects] Starting to load projects...'); const res = await fetch('/api/projects', { credentials: 'same-origin' }); console.log('[Projects] Response status:', res.status, res.statusText); if (!res.ok) { throw new Error(`Failed to load projects (HTTP ${res.status})`); } const data = await res.json(); console.log('[Projects] Response data:', data); projects = data.projects || []; if (loading) loading.style.display = 'none'; if (projects.length === 0) { if (empty) empty.style.display = 'block'; } else { if (grid) { grid.style.display = 'grid'; renderProjectsGrid(projects); } } } catch (err) { console.error('[Projects] Error loading projects:', err); console.error('[Projects] Error stack:', err.stack); console.error('[Projects] Error details:', { message: err.message, name: err.name, toString: err.toString() }); // Report to error monitoring if (typeof reportError === 'function') { reportError({ type: 'console', url: window.location.href, message: 'Error loading projects: ' + err.message, stack: err.stack }); } if (loading) loading.style.display = 'none'; if (error) error.style.display = 'block'; } } /** * Render projects grid */ function renderProjectsGrid(projects) { const grid = document.getElementById('projects-grid'); if (!grid) return; grid.innerHTML = projects.map(project => createProjectCard(project)).join(''); // Add click handlers grid.querySelectorAll('.project-card').forEach(card => { const projectId = card.dataset.projectId; const project = projects.find(p => p.id == projectId); if (project) { card.addEventListener('click', () => openProject(project)); } }); } /** * Create a project card HTML */ function createProjectCard(project) { const name = escapeHtml(project.name); const path = escapeHtml(shortenPath(project.path || '')); const sessionCount = project.sessionCount || 0; const relativeTime = getRelativeTime(project.lastActivity); const icon = project.icon || '📁'; // Determine which sources have been used const sources = project.sources || []; const hasCli = sources.includes('cli'); const hasWeb = sources.includes('web'); let sourcesHtml = ''; if (hasCli && hasWeb) { sourcesHtml = `CLI + Web`; } else if (hasCli) { sourcesHtml = `CLI`; } else if (hasWeb) { sourcesHtml = `Web`; } return `
${icon}

${name}

${path}
💬 ${sessionCount} session${sessionCount !== 1 ? 's' : ''}
🕐 ${relativeTime}
${sourcesHtml}
`; } /** * Open project - show session picker modal */ async function openProject(project) { try { // Load the session picker modal if not already loaded if (!window.SessionPicker) { await loadScript('/claude/claude-ide/components/session-picker-modal.js'); } // Show session picker for this project if (window.SessionPicker) { window.SessionPicker.show(project); } } catch (error) { console.error('Error opening project:', error); showToast('Failed to open project', 'error'); } } /** * Show create project modal */ function showCreateProjectModal() { // For now, use a simple prompt // TODO: Replace with proper modal dialog const name = prompt('Enter project name:'); if (!name) return; const path = prompt('Enter folder path (e.g., ~/projects/my-app):'); if (!path) return; createProject(name, path); } /** * Create a new project */ async function createProject(name, path) { try { showLoadingOverlay('Creating project...'); const res = await fetch('/api/projects', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, path }) }); if (!res.ok) { throw new Error('Failed to create project'); } const data = await res.json(); if (data.success) { hideLoadingOverlay(); showToast('Project created successfully', 'success'); await loadProjects(); // Reload projects list } else { throw new Error(data.error || 'Failed to create project'); } } catch (error) { console.error('Error creating project:', error); hideLoadingOverlay(); showToast(error.message || 'Failed to create project', 'error'); } } /** * Initialize keyboard shortcuts */ function initializeKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Cmd/Ctrl + N - New project / Select folder if ((e.metaKey || e.ctrlKey) && e.key === 'n') { e.preventDefault(); showFolderExplorer(); } // Cmd/Ctrl + R - Refresh projects if ((e.metaKey || e.ctrlKey) && e.key === 'r') { e.preventDefault(); loadProjects(); } }); } /** * Get relative time string */ function getRelativeTime(timestamp) { if (!timestamp) return 'Never'; const date = new Date(timestamp); const now = new Date(); const diffMins = Math.floor((now - date) / 60000); const diffHours = Math.floor((now - date) / 3600000); const diffDays = Math.floor((now - date) / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } /** * Shorten file path for display */ function shortenPath(fullPath) { if (!fullPath) return ''; // Show last 3 parts of path const parts = fullPath.split('/'); if (parts.length > 3) { return '...' + fullPath.slice(fullPath.indexOf('/', fullPath.length - 40)); } return fullPath; } /** * Load a script dynamically */ function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } /** * Show toast notification */ function showToast(message, type = 'info', duration = 3000) { const existingToasts = document.querySelectorAll('.toast-notification'); existingToasts.forEach(toast => toast.remove()); const toast = document.createElement('div'); toast.className = `toast-notification toast-${type}`; toast.innerHTML = ` ${getToastIcon(type)} ${escapeHtml(message)} `; document.body.appendChild(toast); setTimeout(() => { toast.classList.add('visible'); }, 10); setTimeout(() => { toast.classList.remove('visible'); setTimeout(() => { toast.remove(); }, 300); }, duration); } /** * Get toast icon based on type */ function getToastIcon(type) { const icons = { success: '✓', error: '✕', info: 'ℹ', warning: '⚠' }; return icons[type] || icons.info; } /** * Show loading overlay */ function showLoadingOverlay(message = 'Loading...') { let overlay = document.getElementById('loading-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'loading-overlay'; overlay.className = 'loading-overlay'; overlay.innerHTML = `

${escapeHtml(message)}

`; document.body.appendChild(overlay); } else { const textElement = overlay.querySelector('.loading-text'); if (textElement) { textElement.textContent = message; } } overlay.classList.remove('hidden'); setTimeout(() => { overlay.classList.add('visible'); }, 10); } /** * Hide loading overlay */ function hideLoadingOverlay() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.classList.remove('visible'); setTimeout(() => { overlay.classList.add('hidden'); }, 300); } } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Refresh projects function (called from refresh button) function refreshProjects() { loadProjects(); } // Logout function async function logout() { try { await fetch('/claude/api/logout', { method: 'POST' }); window.location.href = '/claude/'; } catch (error) { console.error('Logout failed:', error); } }