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

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

resources/assets/scripts/postline-analytics.tsmodified+47-2
Changes to resources/assets/scripts/postline-analytics.ts
@@ -300,22 +300,67 @@ function fillStats(payload: AnalyticsPayload): void {
300300 }
301301}
302302
303let lastAnalyticsError: string | null = null
304
305function 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
313function 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
303341async function loadAnalytics(): Promise<void> {
304342 if (!analyticsPromise) {
305343 analyticsPromise = fetch('/api/postline/analytics')
306344 .then(async (response) => {
307345 const payload = await response.json()
308346 if (!response.ok || !payload.ok) throw new Error(payload.error || 'Could not load analytics.')
347 lastAnalyticsError = null
309348 return payload.data as AnalyticsPayload
310349 })
311 .catch(() => {
350 .catch((error: unknown) => {
312351 analyticsPromise = null
352 lastAnalyticsError = error instanceof Error ? error.message : 'Could not load analytics.'
313353 return null
314354 })
315355 }
316356
317357 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()
319364 points = data.series || []
320365 fillStats(data)
321366 scheduleAnalyticsRender()
resources/views/analytics.stxmodified+5-0
Changes to resources/views/analytics.stx
@@ -13,6 +13,11 @@
1313 </div>
1414 </header>
1515
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
1621 <div class="grid gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
1722 <article class="grid gap-2 p-3.5 bg-white border border-zinc-200 rounded-2xl shadow-sm">
1823 <span class="text-xs text-zinc-500">Posts this week</span>