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