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/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()