ReviewOS

also looking at this

stacks/postline

fix: MVP-readiness fixes — restore lost UI feedback, register safety, analytics error state, mobile + a11y

#31
Merged glennmichael123 wants to merge fix/readiness-batch-1-feedback-timing into main
7 files +123 -29
resources/views/queue.stxmodified+21-10
Changes to resources/views/queue.stx
@@ -64,10 +64,14 @@
6464 </div>
6565 <strong class="text-[13px]" data-cal-title>—</strong>
6666 </header>
67 <div class="grid grid-cols-7 font-bold text-[11px] text-zinc-500 border-b border-zinc-200">
68 <div class="p-2 text-center">Sun</div><div class="p-2 text-center">Mon</div><div class="p-2 text-center">Tue</div><div class="p-2 text-center">Wed</div><div class="p-2 text-center">Thu</div><div class="p-2 text-center">Fri</div><div class="p-2 text-center">Sat</div>
67 <div class="overflow-x-auto">
68 <div class="min-w-[560px]">
69 <div class="grid grid-cols-7 font-bold text-[11px] text-zinc-500 border-b border-zinc-200">
70 <div class="p-2 text-center">Sun</div><div class="p-2 text-center">Mon</div><div class="p-2 text-center">Tue</div><div class="p-2 text-center">Wed</div><div class="p-2 text-center">Thu</div><div class="p-2 text-center">Fri</div><div class="p-2 text-center">Sat</div>
71 </div>
72 <div class="grid grid-cols-7" data-cal-grid></div>
73 </div>
6974 </div>
70 <div class="grid grid-cols-7" data-cal-grid></div>
7175 </section>
7276 </section>
7377
@@ -80,7 +84,10 @@
8084 const recentList = root.querySelector('[data-queue-recent]')
8185 const meta = root.querySelector('[data-queue-meta]')
8286 const recentMeta = root.querySelector('[data-queue-recent-meta]')
83 const toast = window.postlineToast?.show
87 // Resolve the toast helper at call time — it's a deferred module that
88 // loads after this inline script parses, so an eager capture would be
89 // undefined on a full page load and drop every message to the console.
90 const showToast = options => window.postlineToast?.show?.(options)
8491
8592 const PENDING = new Set(['draft', 'scheduled', 'publishing', 'failed'])
8693
@@ -113,6 +120,9 @@
113120 const liveUrlFor = (uri) => {
114121 const value = String(uri || '')
115122 if (value.startsWith('/')) return value
123 // A stored permalink (Mastodon, and any provider persisting a URL) is
124 // already linkable as-is.
125 if (/^https?:\/\//i.test(value)) return value
116126 const match = value.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/)
117127 return match ? `https://bsky.app/profile/${encodeURIComponent(match[1])}/post/${encodeURIComponent(match[2])}` : null
118128 }
@@ -141,7 +151,7 @@
141151 </header>
142152 <p class="m-0 leading-normal text-[13px] text-slate-700">${escapeHtml(item.body)}</p>
143153 ${failure ? `<p class="m-0 text-red-700 text-xs">${escapeHtml(failure)}</p>` : ''}
144 <footer class="flex gap-2 items-center justify-end">
154 <footer class="flex flex-wrap gap-2 items-center justify-end">
145155 ${liveUrl ? `<a class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-neutral-950 text-xs bg-white hover:bg-zinc-50 border border-zinc-200 rounded-lg shadow-sm transition-colors" href="${escapeHtml(liveUrl)}" target="_blank" rel="noopener noreferrer">View</a>` : ''}
146156 ${actionable ? `<a class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-neutral-950 text-xs bg-white hover:bg-zinc-50 border border-zinc-200 rounded-lg shadow-sm transition-colors" href="/?edit=${item.id}" data-stx-link>Edit</a>` : ''}
147157 ${actionable ? `<button class="inline-flex items-center justify-center px-2.5 h-7 font-bold text-white text-xs bg-neutral-950 hover:bg-neutral-800 rounded-lg transition-colors" type="button" data-queue-publish="${item.id}">Publish now</button>` : ''}
@@ -285,7 +295,7 @@
285295 const message = error instanceof Error ? error.message : String(error)
286296 if (pendingList) pendingList.innerHTML = `<p class="m-0 p-3 text-[13px] text-zinc-500">${escapeHtml(message)}</p>`
287297 if (meta) meta.textContent = 'Could not load'
288 if (toast) toast({ title: 'Queue', message, tone: 'error' })
298 showToast({ title: 'Queue', message, tone: 'error' })
289299 }
290300 }
291301
@@ -307,10 +317,10 @@
307317 publishButton.setAttribute('disabled', 'disabled')
308318 try {
309319 await post('/api/postline/queue/publish-now', publishButton.dataset.queuePublish)
310 if (toast) toast({ title: 'Published', message: 'Post published.', tone: 'success' })
320 showToast({ title: 'Published', message: 'Post published.', tone: 'success' })
311321 }
312322 catch (error) {
313 if (toast) toast({ title: 'Publish failed', message: error instanceof Error ? error.message : String(error), tone: 'error' })
323 showToast({ title: 'Publish failed', message: error instanceof Error ? error.message : String(error), tone: 'error' })
314324 }
315325 await refresh()
316326 return
@@ -318,13 +328,14 @@
318328
319329 const deleteButton = target.closest('[data-queue-delete]')
320330 if (deleteButton) {
331 if (!window.confirm('Delete this post? This cannot be undone.')) return
321332 deleteButton.setAttribute('disabled', 'disabled')
322333 try {
323334 await post('/api/postline/queue/delete', deleteButton.dataset.queueDelete)
324 if (toast) toast({ title: 'Removed', message: 'Post removed from the queue.', tone: 'success' })
335 showToast({ title: 'Removed', message: 'Post removed from the queue.', tone: 'success' })
325336 }
326337 catch (error) {
327 if (toast) toast({ title: 'Delete failed', message: error instanceof Error ? error.message : String(error), tone: 'error' })
338 showToast({ title: 'Delete failed', message: error instanceof Error ? error.message : String(error), tone: 'error' })
328339 }
329340 await refresh()
330341 }