Fix folder explorer error reporting and add logging
- Show actual server error message when project creation fails - Add console logging to debug project creation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
443
public/claude-ide/projects-landing.js
Normal file
443
public/claude-ide/projects-landing.js
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 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) {
|
||||
window.location.href = '/claude/login.html';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user