feat: add projects page JavaScript functionality
Implement complete JavaScript functionality for the projects management page: - State management for projects array and current editing project - loadProjects() - Fetch projects from /api/projects - renderProjects(filter) - Render project cards with search/filter support - setupEventListeners() - Wire up all interactive elements - openProject(projectId) - Navigate to sessions page for selected project - openProjectModal(project) - Open modal for create/edit operations - closeProjectModal() - Close modal and reset form - handleProjectSubmit(e) - Validate and save project data - showProjectMenu(projectId, event) - Display context menu - deleteProject(projectId) - Soft delete with confirmation - openRecycleBinModal() - Display deleted projects - restoreProject(projectId) - Restore from recycle bin - permanentDeleteProject(projectId) - Delete forever with confirmation - escapeHtml(text) - XSS prevention for user-generated content - formatDate(dateString) - Human-readable relative timestamps - showToast(message, type) - Toast notifications with animations Features: - Async/await for all API calls - Comprehensive error handling - Real-time search filtering - Context menu for project actions - Responsive modal system - Toast notifications for user feedback - Keyboard shortcuts (Escape to close) - Click outside to close menus/modals Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
648
public/claude-ide/projects.js
Normal file
648
public/claude-ide/projects.js
Normal file
@@ -0,0 +1,648 @@
|
||||
/**
|
||||
* Projects Page JavaScript
|
||||
* Handles project management, including CRUD operations, search, and recycle bin
|
||||
*/
|
||||
|
||||
// === State Management ===
|
||||
let projects = [];
|
||||
let currentEditingProject = null;
|
||||
let currentContextMenuProjectId = null;
|
||||
|
||||
// === DOM Elements ===
|
||||
const projectsGrid = document.getElementById('projects-grid');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const newProjectBtn = document.getElementById('new-project-btn');
|
||||
const emptyStateNewProjectBtn = document.getElementById('empty-state-new-project-btn');
|
||||
const recycleBinBtn = document.getElementById('recycle-bin-btn');
|
||||
const projectModal = document.getElementById('project-modal');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const projectForm = document.getElementById('project-form');
|
||||
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||
const cancelModalBtn = document.getElementById('cancel-modal-btn');
|
||||
const recycleBinModal = document.getElementById('recycle-bin-modal');
|
||||
const closeRecycleBinBtn = document.getElementById('close-recycle-bin-btn');
|
||||
const recycleBinContent = document.getElementById('recycle-bin-content');
|
||||
const contextMenu = document.getElementById('context-menu');
|
||||
|
||||
// === Initialization ===
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadProjects();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// === Event Listeners Setup ===
|
||||
function setupEventListeners() {
|
||||
// Search functionality
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
renderProjects(e.target.value);
|
||||
});
|
||||
|
||||
// Create project buttons
|
||||
newProjectBtn.addEventListener('click', () => openProjectModal());
|
||||
emptyStateNewProjectBtn.addEventListener('click', () => openProjectModal());
|
||||
|
||||
// Modal close buttons
|
||||
closeModalBtn.addEventListener('click', closeProjectModal);
|
||||
cancelModalBtn.addEventListener('click', closeProjectModal);
|
||||
|
||||
// Project form submission
|
||||
projectForm.addEventListener('submit', handleProjectSubmit);
|
||||
|
||||
// Recycle bin
|
||||
recycleBinBtn.addEventListener('click', openRecycleBinModal);
|
||||
closeRecycleBinBtn.addEventListener('click', () => {
|
||||
recycleBinModal.style.display = 'none';
|
||||
});
|
||||
|
||||
// Context menu actions
|
||||
document.getElementById('ctx-open').addEventListener('click', () => {
|
||||
if (currentContextMenuProjectId) {
|
||||
openProject(currentContextMenuProjectId);
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('ctx-edit').addEventListener('click', () => {
|
||||
if (currentContextMenuProjectId) {
|
||||
const project = projects.find(p => p.id === currentContextMenuProjectId);
|
||||
if (project) {
|
||||
openProjectModal(project);
|
||||
hideContextMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('ctx-delete').addEventListener('click', () => {
|
||||
if (currentContextMenuProjectId) {
|
||||
deleteProject(currentContextMenuProjectId);
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close context menu on click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!contextMenu.contains(e.target)) {
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modals on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeProjectModal();
|
||||
recycleBinModal.style.display = 'none';
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close modals on backdrop click
|
||||
projectModal.addEventListener('click', (e) => {
|
||||
if (e.target === projectModal) {
|
||||
closeProjectModal();
|
||||
}
|
||||
});
|
||||
|
||||
recycleBinModal.addEventListener('click', (e) => {
|
||||
if (e.target === recycleBinModal) {
|
||||
recycleBinModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// === API Functions ===
|
||||
|
||||
/**
|
||||
* Load projects from the server
|
||||
*/
|
||||
async function loadProjects() {
|
||||
try {
|
||||
projectsGrid.innerHTML = '<div class="loading">Loading projects...</div>';
|
||||
|
||||
const response = await fetch('/api/projects');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
projects = data.projects || [];
|
||||
|
||||
renderProjects();
|
||||
} catch (error) {
|
||||
console.error('Error loading projects:', error);
|
||||
showToast('Failed to load projects', 'error');
|
||||
projectsGrid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h2>Error loading projects</h2>
|
||||
<p>${escapeHtml(error.message)}</p>
|
||||
<button onclick="loadProjects()" class="btn-primary">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a project
|
||||
*/
|
||||
async function saveProject(projectData) {
|
||||
try {
|
||||
const url = currentEditingProject
|
||||
? `/api/projects/${currentEditingProject.id}`
|
||||
: '/api/projects';
|
||||
|
||||
const method = currentEditingProject ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(projectData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (currentEditingProject) {
|
||||
// Update existing project in the array
|
||||
const index = projects.findIndex(p => p.id === currentEditingProject.id);
|
||||
if (index !== -1) {
|
||||
projects[index] = result.project;
|
||||
}
|
||||
showToast('Project updated successfully', 'success');
|
||||
} else {
|
||||
// Add new project to the array
|
||||
projects.push(result.project);
|
||||
showToast('Project created successfully', 'success');
|
||||
}
|
||||
|
||||
renderProjects(searchInput.value);
|
||||
closeProjectModal();
|
||||
} catch (error) {
|
||||
console.error('Error saving project:', error);
|
||||
showToast(error.message || 'Failed to save project', 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete a project
|
||||
*/
|
||||
async function deleteProject(projectId) {
|
||||
if (!confirm('Are you sure you want to delete this project? It will be moved to the recycle bin.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// Remove project from array
|
||||
projects = projects.filter(p => p.id !== projectId);
|
||||
renderProjects(searchInput.value);
|
||||
showToast('Project moved to recycle bin', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
showToast(error.message || 'Failed to delete project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load deleted projects for recycle bin
|
||||
*/
|
||||
async function loadDeletedProjects() {
|
||||
try {
|
||||
const response = await fetch('/api/projects?includeDeleted=true');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.projects.filter(p => p.deletedAt) || [];
|
||||
} catch (error) {
|
||||
console.error('Error loading deleted projects:', error);
|
||||
showToast('Failed to load recycle bin', 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a project from recycle bin
|
||||
*/
|
||||
async function restoreProject(projectId) {
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/restore`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// Reload projects and recycle bin
|
||||
await loadProjects();
|
||||
await openRecycleBinModal();
|
||||
showToast('Project restored successfully', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error restoring project:', error);
|
||||
showToast(error.message || 'Failed to restore project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently delete a project
|
||||
*/
|
||||
async function permanentDeleteProject(projectId) {
|
||||
if (!confirm('Are you sure you want to permanently delete this project? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/projects/${projectId}/permanent`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// Reload recycle bin
|
||||
await openRecycleBinModal();
|
||||
showToast('Project permanently deleted', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error permanently deleting project:', error);
|
||||
showToast(error.message || 'Failed to delete project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// === Render Functions ===
|
||||
|
||||
/**
|
||||
* Render project cards based on filter
|
||||
*/
|
||||
function renderProjects(filter = '') {
|
||||
const filteredProjects = projects.filter(project => {
|
||||
const searchTerm = filter.toLowerCase();
|
||||
return (
|
||||
project.name.toLowerCase().includes(searchTerm) ||
|
||||
(project.description && project.description.toLowerCase().includes(searchTerm)) ||
|
||||
project.path.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
|
||||
if (filteredProjects.length === 0) {
|
||||
projectsGrid.style.display = 'none';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
projectsGrid.style.display = 'grid';
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
projectsGrid.innerHTML = filteredProjects.map(project => createProjectCard(project)).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML for a project card
|
||||
*/
|
||||
function createProjectCard(project) {
|
||||
const sessionsCount = project.sessionsCount || 0;
|
||||
const lastActivity = project.lastActivity ? formatDate(project.lastActivity) : 'No activity';
|
||||
|
||||
return `
|
||||
<div class="project-card" data-project-id="${project.id}">
|
||||
<div class="project-card-header">
|
||||
<div class="project-icon" style="background: ${escapeHtml(project.color)}20; color: ${escapeHtml(project.color)}">
|
||||
${escapeHtml(project.icon || '📁')}
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<h3 class="project-name" title="${escapeHtml(project.name)}">${escapeHtml(project.name)}</h3>
|
||||
</div>
|
||||
<button class="project-menu-btn" data-project-id="${project.id}" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem; padding: 0.25rem;">
|
||||
⋮
|
||||
</button>
|
||||
</div>
|
||||
${project.description ? `<p class="project-description">${escapeHtml(project.description)}</p>` : ''}
|
||||
<div class="project-path" title="${escapeHtml(project.path)}">${escapeHtml(project.path)}</div>
|
||||
<div class="project-stats">
|
||||
<div class="project-stat">
|
||||
<span class="project-stat-label">Sessions:</span>
|
||||
<span class="project-stat-value">${sessionsCount}</span>
|
||||
</div>
|
||||
<div class="project-stat">
|
||||
<span class="project-stat-label">Last activity:</span>
|
||||
<span class="project-stat-value">${lastActivity}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render recycle bin content
|
||||
*/
|
||||
async function openRecycleBinModal() {
|
||||
recycleBinModal.style.display = 'flex';
|
||||
recycleBinContent.innerHTML = '<div class="loading">Loading recycle bin...</div>';
|
||||
|
||||
const deletedProjects = await loadDeletedProjects();
|
||||
|
||||
if (deletedProjects.length === 0) {
|
||||
recycleBinContent.innerHTML = `
|
||||
<div class="recycle-bin-empty">
|
||||
<p>Recycle bin is empty</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
recycleBinContent.innerHTML = deletedProjects.map(project => `
|
||||
<div class="recycle-bin-item" data-project-id="${project.id}">
|
||||
<div class="recycle-bin-item-info">
|
||||
<div class="recycle-bin-item-name">${escapeHtml(project.name)}</div>
|
||||
<div class="recycle-bin-item-path">${escapeHtml(project.path)}</div>
|
||||
</div>
|
||||
<div class="recycle-bin-item-actions">
|
||||
<button class="btn-restore" data-project-id="${project.id}">Restore</button>
|
||||
<button class="btn-delete-permanent" data-project-id="${project.id}">Delete Forever</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners for restore and delete buttons
|
||||
recycleBinContent.querySelectorAll('.btn-restore').forEach(btn => {
|
||||
btn.addEventListener('click', () => restoreProject(parseInt(btn.dataset.projectId)));
|
||||
});
|
||||
|
||||
recycleBinContent.querySelectorAll('.btn-delete-permanent').forEach(btn => {
|
||||
btn.addEventListener('click', () => permanentDeleteProject(parseInt(btn.dataset.projectId)));
|
||||
});
|
||||
}
|
||||
|
||||
// === Navigation Functions ===
|
||||
|
||||
/**
|
||||
* Navigate to sessions page for a project
|
||||
*/
|
||||
function openProject(projectId) {
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
if (!project) {
|
||||
showToast('Project not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to sessions page filtered by project
|
||||
window.location.href = `/?projectId=${projectId}`;
|
||||
}
|
||||
|
||||
// === Modal Functions ===
|
||||
|
||||
/**
|
||||
* Open project modal for creating or editing
|
||||
*/
|
||||
function openProjectModal(project = null) {
|
||||
currentEditingProject = project;
|
||||
|
||||
if (project) {
|
||||
modalTitle.textContent = 'Edit Project';
|
||||
document.getElementById('project-name').value = project.name;
|
||||
document.getElementById('project-path').value = project.path;
|
||||
document.getElementById('project-description').value = project.description || '';
|
||||
document.getElementById('project-icon').value = project.icon || '📁';
|
||||
document.getElementById('project-color').value = project.color || '#4a9eff';
|
||||
} else {
|
||||
modalTitle.textContent = 'Create Project';
|
||||
projectForm.reset();
|
||||
document.getElementById('project-color').value = '#4a9eff';
|
||||
}
|
||||
|
||||
projectModal.style.display = 'flex';
|
||||
document.getElementById('project-name').focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close project modal
|
||||
*/
|
||||
function closeProjectModal() {
|
||||
projectModal.style.display = 'none';
|
||||
currentEditingProject = null;
|
||||
projectForm.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle project form submission
|
||||
*/
|
||||
async function handleProjectSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(projectForm);
|
||||
const projectData = {
|
||||
name: formData.get('name').trim(),
|
||||
path: formData.get('path').trim(),
|
||||
description: formData.get('description').trim(),
|
||||
icon: formData.get('icon').trim() || '📁',
|
||||
color: formData.get('color') || '#4a9eff'
|
||||
};
|
||||
|
||||
// Basic validation
|
||||
if (!projectData.name) {
|
||||
showToast('Project name is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectData.path) {
|
||||
showToast('Project path is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await saveProject(projectData);
|
||||
} catch (error) {
|
||||
// Error already handled in saveProject
|
||||
}
|
||||
}
|
||||
|
||||
// === Context Menu Functions ===
|
||||
|
||||
/**
|
||||
* Show context menu for a project
|
||||
*/
|
||||
function showProjectMenu(projectId, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
currentContextMenuProjectId = projectId;
|
||||
|
||||
// Position the menu
|
||||
const menuWidth = 160;
|
||||
const menuHeight = 120;
|
||||
const padding = 10;
|
||||
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
|
||||
// Prevent menu from going off screen
|
||||
if (x + menuWidth > window.innerWidth - padding) {
|
||||
x = window.innerWidth - menuWidth - padding;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight - padding) {
|
||||
y = window.innerHeight - menuHeight - padding;
|
||||
}
|
||||
|
||||
contextMenu.style.left = `${x}px`;
|
||||
contextMenu.style.top = `${y}px`;
|
||||
contextMenu.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide context menu
|
||||
*/
|
||||
function hideContextMenu() {
|
||||
contextMenu.style.display = 'none';
|
||||
currentContextMenuProjectId = null;
|
||||
}
|
||||
|
||||
// === Utility Functions ===
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS attacks
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (typeof text !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date string to human-readable format
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'Unknown';
|
||||
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 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`;
|
||||
|
||||
// For older dates, return formatted date
|
||||
const options = { month: 'short', day: 'numeric', year: 'numeric' };
|
||||
return date.toLocaleDateString('en-US', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
*/
|
||||
function showToast(message, type = 'info') {
|
||||
// Remove existing toast if any
|
||||
const existingToast = document.querySelector('.toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
|
||||
// Add styles
|
||||
Object.assign(toast.style, {
|
||||
position: 'fixed',
|
||||
bottom: '2rem',
|
||||
right: '2rem',
|
||||
padding: '1rem 1.5rem',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6',
|
||||
color: 'white',
|
||||
fontWeight: '500',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
zIndex: '10000',
|
||||
animation: 'slideIn 0.3s ease',
|
||||
maxWidth: '400px',
|
||||
wordWrap: 'break-word'
|
||||
});
|
||||
|
||||
// Add animation keyframes if not already present
|
||||
if (!document.getElementById('toast-animations')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'toast-animations';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// === Event Delegation for Dynamic Elements ===
|
||||
projectsGrid.addEventListener('click', (e) => {
|
||||
const projectCard = e.target.closest('.project-card');
|
||||
const menuBtn = e.target.closest('.project-menu-btn');
|
||||
|
||||
if (menuBtn) {
|
||||
e.stopPropagation();
|
||||
const projectId = parseInt(menuBtn.dataset.projectId);
|
||||
showProjectMenu(projectId, e);
|
||||
} else if (projectCard) {
|
||||
const projectId = parseInt(projectCard.dataset.projectId);
|
||||
openProject(projectId);
|
||||
}
|
||||
});
|
||||
|
||||
// Make functions globally available for HTML event handlers
|
||||
window.loadProjects = loadProjects;
|
||||
window.openProjectModal = openProjectModal;
|
||||
window.closeProjectModal = closeProjectModal;
|
||||
window.openRecycleBinModal = openRecycleBinModal;
|
||||
window.openProject = openProject;
|
||||
window.deleteProject = deleteProject;
|
||||
window.restoreProject = restoreProject;
|
||||
window.permanentDeleteProject = permanentDeleteProject;
|
||||
Reference in New Issue
Block a user