diff options
| author | Arseney300 <Arseney300@gmail.com> | 2026-03-05 09:47:09 +0700 |
|---|---|---|
| committer | Arseney300 <Arseney300@gmail.com> | 2026-03-05 09:47:09 +0700 |
| commit | 0885d20ac18c252f6735cd288448d5d93e95b80f (patch) | |
| tree | edfb6814e97d4000605d9f6a918c30c6aca9e10c /admin-panel/src/client/pages/project.ts | |
| parent | 22749b796d7a5e4efa9494156a3fdcfe39028da7 (diff) | |
WIP: admin_panel: add featurefeature/admin_panel
Diffstat (limited to 'admin-panel/src/client/pages/project.ts')
| -rw-r--r-- | admin-panel/src/client/pages/project.ts | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/admin-panel/src/client/pages/project.ts b/admin-panel/src/client/pages/project.ts new file mode 100644 index 0000000..5e01d64 --- /dev/null +++ b/admin-panel/src/client/pages/project.ts @@ -0,0 +1,167 @@ +import { api, type Task, type Project } from '../lib/api.js'; +import { wsClient } from '../lib/ws.js'; +import { renderBoard } from '../components/board.js'; + +let currentProject: string | null = null; +let cleanupWs: (() => void) | null = null; + +export async function renderProjectPage(container: HTMLElement, projectName: string): Promise<void> { + // Cleanup previous subscriptions + if (currentProject && currentProject !== projectName) { + wsClient.unsubscribe(currentProject); + } + if (cleanupWs) cleanupWs(); + + currentProject = projectName; + + container.innerHTML = ` + <div class="board-header"> + <div style="display:flex;align-items:center;gap:12px"> + <a href="/" data-route="/" class="btn btn-secondary btn-sm">← Back</a> + <h1 id="project-title">Loading...</h1> + </div> + <button class="btn btn-primary" id="new-task-btn">+ New Task</button> + </div> + <div id="board-root"></div> + `; + + const boardRoot = container.querySelector('#board-root') as HTMLElement; + const titleEl = container.querySelector('#project-title')!; + const newTaskBtn = container.querySelector('#new-task-btn')!; + + try { + const [project, tasks] = await Promise.all([ + api.getProject(projectName), + api.getTasks(projectName), + ]); + + titleEl.textContent = project.display_name; + renderBoard(boardRoot, projectName, tasks); + + // WebSocket subscription + wsClient.subscribe(projectName); + const unsub1 = wsClient.on('task:created', (data: unknown) => { + const event = data as { project: string; task: Task }; + if (event.project === projectName) { + refreshBoard(boardRoot, projectName); + } + }); + const unsub2 = wsClient.on('task:updated', (data: unknown) => { + const event = data as { project: string; task: Task }; + if (event.project === projectName) { + refreshBoard(boardRoot, projectName); + } + }); + const unsub3 = wsClient.on('task:deleted', (data: unknown) => { + const event = data as { project: string }; + if (event.project === projectName) { + refreshBoard(boardRoot, projectName); + } + }); + + cleanupWs = () => { unsub1(); unsub2(); unsub3(); }; + + } catch (err) { + boardRoot.innerHTML = `<div class="empty-state"><h3>Failed to load project</h3><p>${(err as Error).message}</p></div>`; + } + + newTaskBtn.addEventListener('click', () => showCreateTaskDialog(projectName, boardRoot)); +} + +async function refreshBoard(boardRoot: HTMLElement, projectName: string): Promise<void> { + try { + const tasks = await api.getTasks(projectName); + renderBoard(boardRoot, projectName, tasks); + } catch { + // silently fail on refresh + } +} + +function showCreateTaskDialog(projectName: string, boardRoot: HTMLElement): void { + 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 Task</h2> + <button class="btn-icon close-btn">×</button> + </div> + <form id="create-task-form"> + <div class="form-group"> + <label for="task-title">Title</label> + <input type="text" id="task-title" placeholder="What needs to be done?" required /> + </div> + <div class="form-group"> + <label for="task-desc">Description</label> + <textarea id="task-desc" rows="4" placeholder="Detailed requirements..."></textarea> + </div> + <div style="display:flex;gap:12px"> + <div class="form-group" style="flex:1"> + <label for="task-priority">Priority</label> + <select id="task-priority"> + <option value="low">Low</option> + <option value="medium" selected>Medium</option> + <option value="high">High</option> + </select> + </div> + <div class="form-group" style="flex:1"> + <label for="task-level">Level</label> + <select id="task-level"> + <option value="1">L1 — Quick</option> + <option value="2">L2 — Standard</option> + <option value="3" selected>L3 — Full</option> + </select> + </div> + </div> + <div class="form-group"> + <label for="task-tags">Tags (comma-separated)</label> + <input type="text" id="task-tags" placeholder="frontend, bug-fix" /> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary close-btn">Cancel</button> + <button type="submit" class="btn btn-primary">Create Task</button> + </div> + </form> + </div> + `; + + document.body.appendChild(overlay); + + overlay.querySelectorAll('.close-btn').forEach(btn => { + btn.addEventListener('click', () => overlay.remove()); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + const form = overlay.querySelector('#create-task-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...'; + + const tagsStr = (overlay.querySelector('#task-tags') as HTMLInputElement).value; + const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : []; + + try { + await api.createTask(projectName, { + title: (overlay.querySelector('#task-title') as HTMLInputElement).value, + description: (overlay.querySelector('#task-desc') as HTMLTextAreaElement).value || undefined, + priority: (overlay.querySelector('#task-priority') as HTMLSelectElement).value, + level: parseInt((overlay.querySelector('#task-level') as HTMLSelectElement).value), + tags, + }); + overlay.remove(); + await refreshBoard(boardRoot, projectName); + } catch (err) { + submitBtn.disabled = false; + submitBtn.textContent = 'Create Task'; + alert((err as Error).message); + } + }); + + (overlay.querySelector('#task-title') as HTMLInputElement).focus(); +} |
