diff options
Diffstat (limited to 'admin-panel/src/client/pages/dashboard.ts')
| -rw-r--r-- | admin-panel/src/client/pages/dashboard.ts | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/admin-panel/src/client/pages/dashboard.ts b/admin-panel/src/client/pages/dashboard.ts new file mode 100644 index 0000000..ded6d3b --- /dev/null +++ b/admin-panel/src/client/pages/dashboard.ts @@ -0,0 +1,150 @@ +import { api, type Project } from '../lib/api.js'; + +export async function renderDashboard(container: HTMLElement): Promise<void> { + container.innerHTML = ` + <div class="dashboard-header"> + <h1>Projects</h1> + <button class="btn btn-primary" id="new-project-btn">+ New Project</button> + </div> + <div class="project-grid" id="project-grid"> + <div class="empty-state"> + <h3>Loading...</h3> + </div> + </div> + `; + + const grid = container.querySelector('#project-grid')!; + const newBtn = container.querySelector('#new-project-btn')!; + + // Load projects + try { + const projects = await api.getProjects(); + renderProjects(grid, projects); + } catch (err) { + grid.innerHTML = `<div class="empty-state"><h3>Failed to load projects</h3><p>${(err as Error).message}</p></div>`; + } + + newBtn.addEventListener('click', () => showCreateDialog(grid)); +} + +function renderProjects(grid: Element, projects: Project[]): void { + if (projects.length === 0) { + grid.innerHTML = ` + <div class="empty-state"> + <h3>No projects yet</h3> + <p>Create your first project to get started.</p> + </div> + `; + return; + } + + grid.innerHTML = projects.map(p => ` + <a href="/project/${p.name}" class="project-card" data-route="/project/${p.name}"> + <h3>${escapeHtml(p.display_name)}</h3> + <div class="project-name">${escapeHtml(p.name)}</div> + ${p.description ? `<div class="project-desc">${escapeHtml(p.description)}</div>` : ''} + <div class="project-stats"> + <span class="stat"><span class="stat-dot todo"></span> ${escapeHtml(p.repo_name)}</span> + <span class="stat">${timeAgo(p.created_at)}</span> + </div> + </a> + `).join(''); +} + +function showCreateDialog(grid: Element): void { + // Remove existing modal if present + document.querySelector('.modal-overlay')?.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` + <div class="modal"> + <div class="modal-header"> + <h2>New Project</h2> + <button class="btn-icon close-btn">×</button> + </div> + <form id="create-project-form"> + <div class="form-group"> + <label for="proj-name">Project Name</label> + <input type="text" id="proj-name" placeholder="my-project" required pattern="[a-z0-9-]+" /> + </div> + <div class="form-group"> + <label for="proj-display">Display Name</label> + <input type="text" id="proj-display" placeholder="My Project" required /> + </div> + <div class="form-group"> + <label for="proj-desc">Description</label> + <textarea id="proj-desc" placeholder="What is this project about?"></textarea> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary close-btn">Cancel</button> + <button type="submit" class="btn btn-primary">Create Project</button> + </div> + </form> + </div> + `; + + document.body.appendChild(overlay); + + // Auto-generate name from display name + const displayInput = overlay.querySelector('#proj-display') as HTMLInputElement; + const nameInput = overlay.querySelector('#proj-name') as HTMLInputElement; + displayInput.addEventListener('input', () => { + nameInput.value = displayInput.value + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-'); + }); + + // Close handlers + overlay.querySelectorAll('.close-btn').forEach(btn => { + btn.addEventListener('click', () => overlay.remove()); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + // Submit + const form = overlay.querySelector('#create-project-form') as HTMLFormElement; + form.addEventListener('submit', async (e) => { + e.preventDefault(); + const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement; + submitBtn.disabled = true; + submitBtn.textContent = 'Creating...'; + + try { + await api.createProject({ + name: nameInput.value, + display_name: displayInput.value, + description: (overlay.querySelector('#proj-desc') as HTMLTextAreaElement).value || undefined, + }); + overlay.remove(); + // Reload projects + const projects = await api.getProjects(); + renderProjects(grid, projects); + } catch (err) { + submitBtn.disabled = false; + submitBtn.textContent = 'Create Project'; + alert((err as Error).message); + } + }); + + displayInput.focus(); +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function timeAgo(dateStr: string): string { + const date = new Date(dateStr + 'Z'); + const now = new Date(); + const diff = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; +} |
