also looking at this
fix: MVP-readiness fixes — restore lost UI feedback, register safety, analytics error state, mobile + a11y
#31
7 files
+123
-29
| @@ -300,22 +300,67 @@ function fillStats(payload: AnalyticsPayload): void { | ||
| 300 | 300 | } |
| 301 | 301 | } |
| 302 | 302 | |
| 303 | let lastAnalyticsError: string | null = null | |
| 304 | ||
| 305 | function hideAnalyticsError(): void { | |
| 306 | const banner = document.querySelector<HTMLElement>('[data-analytics-error]') | |
| 307 | if (banner) { | |
| 308 | banner.classList.add('hidden') | |
| 309 | banner.classList.remove('flex') | |
| 310 | } | |
| 311 | } | |
| 312 | ||
| 313 | function showAnalyticsError(message: string): void { | |
| 314 | const page = document.querySelector<HTMLElement>('[data-testid="analytics-page"]') | |
| 315 | if (!page) | |
| 316 | return | |
| 317 | ||
| 318 | const msg = page.querySelector('[data-analytics-error-message]') | |
| 319 | if (msg) | |
| 320 | msg.textContent = message | |
| 321 | const banner = page.querySelector<HTMLElement>('[data-analytics-error]') | |
| 322 | if (banner) { | |
| 323 | banner.classList.remove('hidden') | |
| 324 | banner.classList.add('flex') | |
| 325 | } | |
| 326 | ||
| 327 | // Stop the stat trends from claiming they're still loading forever. | |
| 328 | page.querySelectorAll('[data-analytics-posts-trend], [data-analytics-engagement-trend], [data-analytics-best-channel-trend], [data-analytics-best-window-trend]') | |
| 329 | .forEach((el) => { if (el.textContent === 'Loading…') el.textContent = 'Unavailable' }) | |
| 330 | ||
| 331 | const retry = page.querySelector<HTMLElement>('[data-analytics-retry]') | |
| 332 | if (retry && !retry.dataset.wired) { | |
| 333 | retry.dataset.wired = 'true' | |
| 334 | retry.addEventListener('click', () => { | |
| 335 | hideAnalyticsError() | |
| 336 | void loadAnalytics() | |
| 337 | }) | |
| 338 | } | |
| 339 | } | |
| 340 | ||
| 303 | 341 | async function loadAnalytics(): Promise<void> { |
| 304 | 342 | if (!analyticsPromise) { |
| 305 | 343 | analyticsPromise = fetch('/api/postline/analytics') |
| 306 | 344 | .then(async (response) => { |
| 307 | 345 | const payload = await response.json() |
| 308 | 346 | if (!response.ok || !payload.ok) throw new Error(payload.error || 'Could not load analytics.') |
| 347 | lastAnalyticsError = null | |
| 309 | 348 | return payload.data as AnalyticsPayload |
| 310 | 349 | }) |
| 311 | .catch(() => { | |
| 350 | .catch((error: unknown) => { | |
| 312 | 351 | analyticsPromise = null |
| 352 | lastAnalyticsError = error instanceof Error ? error.message : 'Could not load analytics.' | |
| 313 | 353 | return null |
| 314 | 354 | }) |
| 315 | 355 | } |
| 316 | 356 | |
| 317 | 357 | const data = await analyticsPromise |
| 318 | if (!data) return | |
| 358 | if (!data) { | |
| 359 | // Previously returned silently, leaving the dashboard stuck on "Loading…". | |
| 360 | showAnalyticsError(lastAnalyticsError || 'Could not load analytics.') | |
| 361 | return | |
| 362 | } | |
| 363 | hideAnalyticsError() | |
| 319 | 364 | points = data.series || [] |
| 320 | 365 | fillStats(data) |
| 321 | 366 | scheduleAnalyticsRender() |
| @@ -37,7 +37,7 @@ | ||
| 37 | 37 | <span class="text-xs text-zinc-500" data-bluesky-channel-pill>Loading…</span> |
| 38 | 38 | </header> |
| 39 | 39 | <div class="grid gap-2.5 p-3" data-bluesky-channels> |
| 40 | <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="bluesky"> | |
| 40 | <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="bluesky"> | |
| 41 | 41 | <div> |
| 42 | 42 | <h2 class="my-0 font-bold text-[15px]">Bluesky</h2> |
| 43 | 43 | <p class="my-1.5 text-[13px] text-slate-700" data-bluesky-channel-handle>Not connected</p> |
| @@ -45,7 +45,7 @@ | ||
| 45 | 45 | </div> |
| 46 | 46 | <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-bluesky-channel-status>Setup</span> |
| 47 | 47 | </article> |
| 48 | <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="linkedin"> | |
| 48 | <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="linkedin"> | |
| 49 | 49 | <div> |
| 50 | 50 | <h2 class="my-0 font-bold text-[15px]">LinkedIn</h2> |
| 51 | 51 | <p class="my-1.5 text-[13px] text-slate-700" data-linkedin-channel-handle>Not connected</p> |
| @@ -57,7 +57,7 @@ | ||
| 57 | 57 | <button class="hidden text-xs text-zinc-500 underline hover:text-zinc-700 transition-colors" type="button" data-linkedin-connect-env>Use token from .env</button> |
| 58 | 58 | </div> |
| 59 | 59 | </article> |
| 60 | <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="instagram"> | |
| 60 | <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="instagram"> | |
| 61 | 61 | <div> |
| 62 | 62 | <h2 class="my-0 font-bold text-[15px]">Instagram</h2> |
| 63 | 63 | <p class="my-1.5 text-[13px] text-slate-700" data-instagram-channel-handle>Not connected</p> |
| @@ -69,7 +69,7 @@ | ||
| 69 | 69 | <button class="hidden text-xs text-zinc-500 underline hover:text-zinc-700 transition-colors" type="button" data-instagram-connect-env>Use token from .env</button> |
| 70 | 70 | </div> |
| 71 | 71 | </article> |
| 72 | <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="threads"> | |
| 72 | <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="threads"> | |
| 73 | 73 | <div> |
| 74 | 74 | <h2 class="my-0 font-bold text-[15px]">Threads</h2> |
| 75 | 75 | <p class="my-1.5 text-[13px] text-slate-700" data-threads-channel-handle>Not connected</p> |
| @@ -91,13 +91,13 @@ | ||
| 91 | 91 | <span class="px-2.5 py-1 font-bold text-[11px] text-zinc-500 whitespace-nowrap border border-zinc-200 rounded-full" data-mastodon-channel-status>Setup</span> |
| 92 | 92 | </div> |
| 93 | 93 | <form class="grid gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)_auto]" data-mastodon-connect-form> |
| 94 | <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="instance" placeholder="mastodon.social" autocomplete="url" data-mastodon-instance required> | |
| 95 | <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="access_token" type="password" placeholder="Access token" autocomplete="off" data-mastodon-token required> | |
| 94 | <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="instance" placeholder="mastodon.social" autocomplete="url" aria-label="Mastodon instance" data-mastodon-instance required> | |
| 95 | <input class="px-3 h-9 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="access_token" type="password" placeholder="Access token" autocomplete="off" aria-label="Mastodon access token" data-mastodon-token required> | |
| 96 | 96 | <button class="inline-flex items-center justify-center px-4 h-9 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-lg disabled:opacity-50 transition-colors" type="submit" data-mastodon-connect-submit>Connect</button> |
| 97 | 97 | <p class="sm:col-span-3 m-0 text-xs text-zinc-500">Create a token in your instance's Preferences → Development → New application (needs the <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">write:statuses</code> + <code class="px-1 py-0.5 text-xs bg-zinc-100 rounded">write:media</code> scopes).</p> |
| 98 | 98 | </form> |
| 99 | 99 | </article> |
| 100 | <article class="flex gap-5 items-center justify-between p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="twitter"> | |
| 100 | <article class="flex flex-col gap-3 justify-between sm:flex-row sm:gap-5 sm:items-center p-3.5 bg-white border border-zinc-200 rounded-xl" data-channel="twitter"> | |
| 101 | 101 | <div> |
| 102 | 102 | <h2 class="my-0 font-bold text-[15px]">X (Twitter)</h2> |
| 103 | 103 | <p class="my-1.5 text-[13px] text-slate-700" data-twitter-channel-handle>Not connected</p> |
| @@ -126,11 +126,12 @@ | ||
| 126 | 126 | const detail = root.querySelector('[data-bluesky-channel-detail]') |
| 127 | 127 | const status = root.querySelector('[data-bluesky-channel-status]') |
| 128 | 128 | const pill = root.querySelector('[data-bluesky-channel-pill]') |
| 129 | const toast = window.postlineToast?.show | |
| 130 | const blueskyApi = window.postlineBluesky | |
| 131 | ||
| 132 | 129 | const notify = (message, tone = 'neutral', options = {}) => { |
| 133 | if (toast) toast({ message, tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info', title: options.title }) | |
| 130 | // Resolve the toast helper at call time — it's a deferred module that | |
| 131 | // loads after this inline script parses, so an eager capture is | |
| 132 | // undefined on a full page load (e.g. the OAuth redirect back here). | |
| 133 | const show = window.postlineToast?.show | |
| 134 | if (show) show({ message, tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info', title: options.title }) | |
| 134 | 135 | else console.log(message) |
| 135 | 136 | } |
| 136 | 137 | |
| @@ -162,7 +163,7 @@ | ||
| 162 | 163 | |
| 163 | 164 | const refreshStatus = async () => { |
| 164 | 165 | try { |
| 165 | const data = await blueskyApi?.fetchStatus() | |
| 166 | const data = await window.postlineBluesky?.fetchStatus() | |
| 166 | 167 | renderStatus(data) |
| 167 | 168 | } |
| 168 | 169 | catch (error) { |
| @@ -658,7 +659,17 @@ | ||
| 658 | 659 | startMastodon() |
| 659 | 660 | } |
| 660 | 661 | |
| 661 | void refreshStatus() | |
| 662 | // Gate the initial status fetch on DOMContentLoaded so the deferred | |
| 663 | // postline-bluesky module has loaded — otherwise a connected account | |
| 664 | // renders "Not connected" on a full page load. (Mirrors the other | |
| 665 | // providers.) | |
| 666 | const startBluesky = () => void refreshStatus() | |
| 667 | if (document.readyState === 'loading') { | |
| 668 | document.addEventListener('DOMContentLoaded', startBluesky, { once: true }) | |
| 669 | } | |
| 670 | else { | |
| 671 | startBluesky() | |
| 672 | } | |
| 662 | 673 | })() |
| 663 | 674 | </script> |
| 664 | 675 | @endsection |
| @@ -13,6 +13,11 @@ | ||
| 13 | 13 | </div> |
| 14 | 14 | </header> |
| 15 | 15 | |
| 16 | <div class="hidden gap-3 items-center justify-between p-3.5 text-[13px] text-red-700 bg-red-50 border border-red-200 rounded-2xl" data-analytics-error> | |
| 17 | <span data-analytics-error-message>Couldn't load analytics.</span> | |
| 18 | <button class="inline-flex items-center justify-center px-3 h-8 font-bold text-white text-xs bg-neutral-950 hover:bg-neutral-800 rounded-lg transition-colors" type="button" data-analytics-retry>Retry</button> | |
| 19 | </div> | |
| 20 | ||
| 16 | 21 | <div class="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4"> |
| 17 | 22 | <article class="grid gap-2 p-3.5 bg-white border border-zinc-200 rounded-2xl shadow-sm"> |
| 18 | 23 | <span class="text-xs text-zinc-500">Posts this week</span> |
| @@ -113,7 +113,6 @@ | ||
| 113 | 113 | const connectionLabel = root.querySelector('[data-bluesky-connection-label]') |
| 114 | 114 | const publishLabel = root.querySelector('[data-bluesky-publish-label]') |
| 115 | 115 | const buttons = Array.from(root.querySelectorAll('[data-bluesky-publish], [data-bluesky-publish-top]')) |
| 116 | const toast = window.postlineToast?.show | |
| 117 | 116 | const providerToggles = root.querySelector('[data-provider-toggles]') |
| 118 | 117 | const composeImageRow = root.querySelector('[data-compose-image-row]') |
| 119 | 118 | const composeImage = root.querySelector('[data-compose-image]') |
| @@ -162,8 +161,13 @@ | ||
| 162 | 161 | let editRemoveImage = false |
| 163 | 162 | |
| 164 | 163 | const setStatus = (message, tone = 'neutral', options = {}) => { |
| 165 | if (toast) { | |
| 166 | toast({ | |
| 164 | // Resolve the toast helper at call time. It's a deferred module that | |
| 165 | // loads AFTER this inline script parses, so capturing it eagerly (as | |
| 166 | // this used to) left it undefined on every full page load — silently | |
| 167 | // dropping all feedback to the console. | |
| 168 | const show = window.postlineToast?.show | |
| 169 | if (show) { | |
| 170 | show({ | |
| 167 | 171 | title: options.title, |
| 168 | 172 | message, |
| 169 | 173 | tone: tone === 'success' ? 'success' : tone === 'error' ? 'error' : 'info', |
| @@ -446,7 +446,7 @@ | ||
| 446 | 446 | |
| 447 | 447 | // Number keys 1-6 jump between pages (the sidebar shows the badges). |
| 448 | 448 | const bindKeyboardShortcuts = () => { |
| 449 | const routes = { 1: '/', 2: '/queue', 3: '/timeline', 4: '/analytics', 5: '/accounts', 6: '/blog', 7: '/settings' } | |
| 449 | const routes = { 1: '/', 2: '/queue', 3: '/timeline', 4: '/analytics', 5: '/accounts', 6: '/settings' } | |
| 450 | 450 | document.addEventListener('keydown', (event) => { |
| 451 | 451 | if (event.metaKey || event.ctrlKey || event.altKey) return |
| 452 | 452 | const target = event.target |
| @@ -51,6 +51,10 @@ | ||
| 51 | 51 | <span class="font-bold text-xs text-zinc-500">Password</span> |
| 52 | 52 | <input class="px-3 h-10 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required minlength="6" data-login-password> |
| 53 | 53 | </label> |
| 54 | <label class="hidden gap-1.5" data-login-confirm-row> | |
| 55 | <span class="font-bold text-xs text-zinc-500">Confirm password</span> | |
| 56 | <input class="px-3 h-10 text-neutral-950 text-sm bg-white border border-zinc-200 outline-none rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-100" name="password_confirmation" type="password" autocomplete="new-password" placeholder="••••••••" minlength="6" data-login-confirm> | |
| 57 | </label> | |
| 54 | 58 | <p class="hidden m-0 text-red-700 text-xs" data-login-error></p> |
| 55 | 59 | <button class="inline-flex items-center justify-center px-4 h-10 font-bold text-sm text-white bg-neutral-950 hover:bg-neutral-800 rounded-xl disabled:opacity-50 transition-colors" type="submit" data-login-submit>Sign in</button> |
| 56 | 60 | </form> |
| @@ -71,6 +75,8 @@ | ||
| 71 | 75 | const nameInput = root.querySelector('[data-login-name]') |
| 72 | 76 | const emailInput = root.querySelector('[data-login-email]') |
| 73 | 77 | const passwordInput = root.querySelector('[data-login-password]') |
| 78 | const confirmRow = root.querySelector('[data-login-confirm-row]') | |
| 79 | const confirmInput = root.querySelector('[data-login-confirm]') | |
| 74 | 80 | const errorText = root.querySelector('[data-login-error]') |
| 75 | 81 | const submit = root.querySelector('[data-login-submit]') |
| 76 | 82 | const toggle = root.querySelector('[data-login-toggle]') |
| @@ -87,6 +93,12 @@ | ||
| 87 | 93 | registering = register |
| 88 | 94 | nameRow?.classList.toggle('hidden', !register) |
| 89 | 95 | nameRow?.classList.toggle('grid', register) |
| 96 | confirmRow?.classList.toggle('hidden', !register) | |
| 97 | confirmRow?.classList.toggle('grid', register) | |
| 98 | // The workspace-owning account has no recovery path, so a first-run | |
| 99 | // typo must not silently create an account nobody can sign into. | |
| 100 | nameInput?.toggleAttribute('required', register) | |
| 101 | confirmInput?.toggleAttribute('required', register) | |
| 90 | 102 | if (heading) heading.textContent = register ? 'Create your workspace' : 'Sign in to Postline' |
| 91 | 103 | if (subtitle) subtitle.textContent = register ? 'One account owns this Postline instance.' : 'Your personal publishing workspace.' |
| 92 | 104 | if (submit) submit.textContent = register ? 'Create account' : 'Sign in' |
| @@ -105,6 +117,12 @@ | ||
| 105 | 117 | form?.addEventListener('submit', async (event) => { |
| 106 | 118 | event.preventDefault() |
| 107 | 119 | setError('') |
| 120 | ||
| 121 | if (registering && (passwordInput?.value || '') !== (confirmInput?.value || '')) { | |
| 122 | setError('Passwords do not match.') | |
| 123 | return | |
| 124 | } | |
| 125 | ||
| 108 | 126 | submit?.setAttribute('disabled', 'disabled') |
| 109 | 127 | |
| 110 | 128 | const body = new FormData() |
| @@ -64,10 +64,14 @@ | ||
| 64 | 64 | </div> |
| 65 | 65 | <strong class="text-[13px]" data-cal-title>—</strong> |
| 66 | 66 | </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> | |
| 69 | 74 | </div> |
| 70 | <div class="grid grid-cols-7" data-cal-grid></div> | |
| 71 | 75 | </section> |
| 72 | 76 | </section> |
| 73 | 77 | |
| @@ -80,7 +84,10 @@ | ||
| 80 | 84 | const recentList = root.querySelector('[data-queue-recent]') |
| 81 | 85 | const meta = root.querySelector('[data-queue-meta]') |
| 82 | 86 | 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) | |
| 84 | 91 | |
| 85 | 92 | const PENDING = new Set(['draft', 'scheduled', 'publishing', 'failed']) |
| 86 | 93 | |
| @@ -113,6 +120,9 @@ | ||
| 113 | 120 | const liveUrlFor = (uri) => { |
| 114 | 121 | const value = String(uri || '') |
| 115 | 122 | 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 | |
| 116 | 126 | const match = value.match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/(.+)$/) |
| 117 | 127 | return match ? `https://bsky.app/profile/${encodeURIComponent(match[1])}/post/${encodeURIComponent(match[2])}` : null |
| 118 | 128 | } |
| @@ -141,7 +151,7 @@ | ||
| 141 | 151 | </header> |
| 142 | 152 | <p class="m-0 leading-normal text-[13px] text-slate-700">${escapeHtml(item.body)}</p> |
| 143 | 153 | ${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"> | |
| 145 | 155 | ${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>` : ''} |
| 146 | 156 | ${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>` : ''} |
| 147 | 157 | ${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 @@ | ||
| 285 | 295 | const message = error instanceof Error ? error.message : String(error) |
| 286 | 296 | if (pendingList) pendingList.innerHTML = `<p class="m-0 p-3 text-[13px] text-zinc-500">${escapeHtml(message)}</p>` |
| 287 | 297 | if (meta) meta.textContent = 'Could not load' |
| 288 | if (toast) toast({ title: 'Queue', message, tone: 'error' }) | |
| 298 | showToast({ title: 'Queue', message, tone: 'error' }) | |
| 289 | 299 | } |
| 290 | 300 | } |
| 291 | 301 | |
| @@ -307,10 +317,10 @@ | ||
| 307 | 317 | publishButton.setAttribute('disabled', 'disabled') |
| 308 | 318 | try { |
| 309 | 319 | 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' }) | |
| 311 | 321 | } |
| 312 | 322 | 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' }) | |
| 314 | 324 | } |
| 315 | 325 | await refresh() |
| 316 | 326 | return |
| @@ -318,13 +328,14 @@ | ||
| 318 | 328 | |
| 319 | 329 | const deleteButton = target.closest('[data-queue-delete]') |
| 320 | 330 | if (deleteButton) { |
| 331 | if (!window.confirm('Delete this post? This cannot be undone.')) return | |
| 321 | 332 | deleteButton.setAttribute('disabled', 'disabled') |
| 322 | 333 | try { |
| 323 | 334 | 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' }) | |
| 325 | 336 | } |
| 326 | 337 | 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' }) | |
| 328 | 339 | } |
| 329 | 340 | await refresh() |
| 330 | 341 | } |