Files
SuperCharged-Claude-Code-Up…/public/claude-ide/projects-landing.js
uroma f6ba241062 Add login modal to landing page
- Show login modal when user is not authenticated
- Add handleLogin, showLoginModal, closeLoginModal functions
- Add login modal HTML with username/password form
- Add CSS styles for modal

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-21 14:46:19 +00:00

507 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `<span class="source-badge both">CLI + Web</span>`;
} else if (hasCli) {
sourcesHtml = `<span class="source-badge cli">CLI</span>`;
} else if (hasWeb) {
sourcesHtml = `<span class="source-badge web">Web</span>`;
}
return `
<div class="project-card" data-project-id="${project.id}">
<div class="project-card-header">
<div class="project-icon">${icon}</div>
<div class="project-info">
<h3 class="project-name">${name}</h3>
<div class="project-path">${path}</div>
</div>
</div>
<div class="project-meta">
<div class="meta-item session-count">
<span class="icon">💬</span>
<span>${sessionCount} session${sessionCount !== 1 ? 's' : ''}</span>
</div>
<div class="meta-item last-activity">
<span class="icon">🕐</span>
<span>${relativeTime}</span>
</div>
<div class="project-sources">
${sourcesHtml}
</div>
</div>
</div>
`;
}
/**
* 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 = `
<span class="toast-icon">${getToastIcon(type)}</span>
<span class="toast-message">${escapeHtml(message)}</span>
`;
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 = `
<div class="loading-spinner"></div>
<p class="loading-text">${escapeHtml(message)}</p>
`;
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);
}
}