summaryrefslogtreecommitdiff
path: root/admin-panel/src/client/pages/project.ts
diff options
context:
space:
mode:
authorArseney300 <Arseney300@gmail.com>2026-03-05 09:47:09 +0700
committerArseney300 <Arseney300@gmail.com>2026-03-05 09:47:09 +0700
commit0885d20ac18c252f6735cd288448d5d93e95b80f (patch)
treeedfb6814e97d4000605d9f6a918c30c6aca9e10c /admin-panel/src/client/pages/project.ts
parent22749b796d7a5e4efa9494156a3fdcfe39028da7 (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.ts167
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">&larr; 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">&times;</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();
+}