From e61328aa4ef66df214a5d6baac2e5e641ad4abe6 Mon Sep 17 00:00:00 2001 From: claudecode Date: Wed, 1 Jul 2026 18:24:53 -0400 Subject: [PATCH] Replace thread status dropdown/text with inline status buttons Card and modal both use a clickable button row for status instead of static text or a select, so changing status no longer requires opening the full edit form. Co-Authored-By: Claude Sonnet 5 --- public/css/styles.css | 65 +++++++++++++++++++++++ public/js/threads.js | 121 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 171 insertions(+), 15 deletions(-) diff --git a/public/css/styles.css b/public/css/styles.css index a7912b4..3705dbe 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1042,6 +1042,39 @@ h1, h2, h3, h4, h5, h6 { white-space: pre-wrap; } +.thread-status-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 2px; +} + +.thread-status-btn { + padding: 6px 14px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--bg); + color: var(--text-secondary); + font-family: var(--font-heading); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: border-color var(--transition-speed) ease, color var(--transition-speed) ease, background var(--transition-speed) ease; +} + +.thread-status-btn:hover { + color: var(--text-primary); + border-color: var(--accent-hover); +} + +.thread-status-btn.active { + border-color: var(--accent); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, transparent); +} + .thread-card-actions { display: flex; gap: 10px; @@ -1162,6 +1195,38 @@ h1, h2, h3, h4, h5, h6 { border-color: var(--accent-hover); } +.modal-status-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.modal-status-btn { + padding: 8px 14px; + border: 1px solid var(--border); + border-radius: 3px; + background: var(--bg); + color: var(--text-secondary); + font-family: var(--font-heading); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: border-color var(--transition-speed) ease, color var(--transition-speed) ease, background var(--transition-speed) ease; +} + +.modal-status-btn:hover { + color: var(--text-primary); + border-color: var(--accent-hover); +} + +.modal-status-btn.active { + border-color: var(--accent); + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, transparent); +} + .modal-actions { display: flex; gap: 10px; diff --git a/public/js/threads.js b/public/js/threads.js index ea3e5d8..0395f3a 100644 --- a/public/js/threads.js +++ b/public/js/threads.js @@ -133,17 +133,22 @@ function buildThreadCard(thread) { if (expandedId === thread.id) { card.classList.add('expanded'); - card.appendChild(buildThreadDetail(thread)); + card.appendChild(buildThreadDetail(thread, card)); } return card; } -function buildThreadDetail(thread) { +function buildThreadDetail(thread, card) { const detail = document.createElement('div'); detail.className = 'thread-card-detail'; FIELD_DEFS.forEach(({ key, label }) => { + if (key === 'status') { + detail.appendChild(buildStatusButtonRow(thread, card)); + return; + } + const row = document.createElement('div'); row.className = 'thread-field'; @@ -153,7 +158,7 @@ function buildThreadDetail(thread) { const valueEl = document.createElement('span'); valueEl.className = 'thread-field-value'; - valueEl.textContent = key === 'status' ? statusLabel(thread.status) : (thread[key] || '—'); + valueEl.textContent = thread[key] || '—'; row.append(labelEl, valueEl); detail.appendChild(row); @@ -180,6 +185,66 @@ function buildThreadDetail(thread) { return detail; } +function buildStatusButtonRow(thread, card) { + const row = document.createElement('div'); + row.className = 'thread-field'; + + const labelEl = document.createElement('span'); + labelEl.className = 'thread-field-label'; + labelEl.textContent = 'Status'; + row.appendChild(labelEl); + + const buttonRow = document.createElement('div'); + buttonRow.className = 'thread-status-buttons'; + + STATUSES.forEach((status) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'thread-status-btn'; + btn.dataset.status = status; + btn.classList.toggle('active', status === thread.status); + btn.textContent = statusLabel(status); + btn.addEventListener('click', () => handleStatusChange(thread, status, card)); + buttonRow.appendChild(btn); + }); + + row.appendChild(buttonRow); + return row; +} + +async function handleStatusChange(thread, newStatus, card) { + if (newStatus === thread.status) return; + + const campaign = getActiveCampaign(); + if (!campaign) return; + + const payload = {}; + FIELD_DEFS.forEach(({ key }) => { + payload[key] = thread[key]; + }); + payload.status = newStatus; + + try { + const updated = await updateThread(campaign.id, thread.id, payload); + Object.assign(thread, updated); + updateCardStatusUI(card, thread); + } catch (err) { + alert(err.message); + } +} + +function updateCardStatusUI(card, thread) { + const badge = card.querySelector('.thread-status-badge'); + if (badge) { + badge.className = `thread-status-badge thread-status-${thread.status}`; + badge.textContent = statusLabel(thread.status); + } + + card.querySelectorAll('.thread-status-btn').forEach((btn) => { + btn.classList.toggle('active', btn.dataset.status === thread.status); + }); +} + function buildModalForm(values) { const body = container.querySelector('#threadModalBody'); body.innerHTML = ''; @@ -191,21 +256,18 @@ function buildModalForm(values) { const labelEl = document.createElement('label'); labelEl.className = 'modal-field-label'; labelEl.textContent = label; - labelEl.setAttribute('for', `thread-field-${key}`); group.appendChild(labelEl); - let input; if (type === 'select') { - input = document.createElement('select'); - input.className = 'modal-select'; - STATUSES.forEach((status) => { - const option = document.createElement('option'); - option.value = status; - option.textContent = statusLabel(status); - input.appendChild(option); - }); - input.value = values.status || 'active'; - } else if (type === 'textarea') { + group.appendChild(buildModalStatusButtons(values.status || 'active')); + body.appendChild(group); + return; + } + + labelEl.setAttribute('for', `thread-field-${key}`); + + let input; + if (type === 'textarea') { input = document.createElement('textarea'); input.className = 'modal-textarea'; input.rows = 3; @@ -223,6 +285,35 @@ function buildModalForm(values) { }); } +function buildModalStatusButtons(currentStatus) { + const wrapper = document.createElement('div'); + wrapper.className = 'modal-status-buttons'; + + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.id = 'thread-field-status'; + hiddenInput.value = currentStatus; + wrapper.appendChild(hiddenInput); + + STATUSES.forEach((status) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'modal-status-btn'; + btn.dataset.status = status; + btn.classList.toggle('active', status === currentStatus); + btn.textContent = statusLabel(status); + btn.addEventListener('click', () => { + hiddenInput.value = status; + wrapper.querySelectorAll('.modal-status-btn').forEach((b) => { + b.classList.toggle('active', b.dataset.status === status); + }); + }); + wrapper.appendChild(btn); + }); + + return wrapper; +} + function collectFormData() { const data = {}; FIELD_DEFS.forEach(({ key }) => {