summaryrefslogtreecommitdiff
path: root/admin-panel/src/client/pages/dashboard.ts
diff options
context:
space:
mode:
Diffstat (limited to 'admin-panel/src/client/pages/dashboard.ts')
-rw-r--r--admin-panel/src/client/pages/dashboard.ts150
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">&times;</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`;
+}