Skip to content

web UI: style press/events/newsletter content screens - #60

Merged
TSVRN9 merged 3 commits into
mainfrom
ux/content-screens
Aug 19, 2026
Merged

TSVRN9 merged 3 commits into
mainfrom
ux/content-screens

Conversation

@TSVRN9

@TSVRN9 TSVRN9 commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

Closes #41

Scope change acknowledged

#41's original body listed five screens (press/events/faq/staff/newsletter). Per the issue's 2026-08-19 comment, FAQ and Staff Directory are cut from the product entirely (#50/#51, already deleted from the codebase before this branch started). This PR styles only press, events, newsletter. Repo-wide grep for remnants before starting, zero hits:

grep -ril "faq\|staff" web/src shared/src mobile/src
# (no output)

Acceptance criteria (as descoped)

  • Press, events, and newsletter restyled with web UI: install Tailwind CSS + design tokens + styled shared layout #35/web UX: shared component layer, loading/error states, and nav hierarchy in the app shell #43's shared component classes (.page-title, .label-rule, .card, .empty-state, .badge), sharing one list/card pattern instead of independent bare <ul><li> markup on each screen.
  • Staff directory photos display well if present, degrade gracefully if absent — void, staff directory is cut. Generalized the same principle to the two screens that do carry remote images: press release images and event featured images. Both wrapped in {#if} (absent → no <img> at all, no broken-image icon) and, for present images, object-contain (not object-cover) so nothing gets cropped. Verified against live upstream data during the check below that UMass Dining's event images are ~1024×432 banners, not square thumbnails — a fixed square object-cover box would have sliced most of the banner off, so events get a wider h-16 w-28 box than press's h-16 w-16.
  • pnpm --filter web check clean (0 errors, 0 warnings, 396 files).
  • CI: this diff only touches files under web/, so the CI lanes it can affect are web-check and e2e (shared/mobile/supabase-* jobs are untouched and unaffected). Both run clean locally, see below; I don't have a way to trigger the GitHub Actions run itself from here, so the PR checks are the live confirmation.

Anonymous-first / no auth gates

Zero session/auth code in any of the three +page.ts/+page.svelte files, unchanged from before this PR — these are public content screens fetched via /api/{press,events,newsletter}, no auth.uid() or session check anywhere in the diff.

What changed

  • web/src/routes/press/+page.svelte
  • web/src/routes/events/+page.svelte
  • web/src/routes/newsletter/+page.svelte
  • web/e2e/content-screens.spec.ts (new)

Not touched: web/src/app.css, web/src/routes/+layout.svelte, any other route, any other e2e spec.

Red/green TDD evidence

Wrote content-screens.spec.ts first, against the unstyled baseline pages. 6 of 9 tests are new UI behavior (styling classes that didn't exist yet) and went red naturally:

PORT=4304 npx playwright test e2e/content-screens.spec.ts --reporter=line
  6 failed
    press › releases render as cards sharing the site's list/card pattern...
    press › no releases shows the shared empty-state panel...
    events › events render as cards with a featured badge...
    events › no events shows the shared empty-state panel
    newsletter › issues render as cards linking out to the external archive
    newsletter › no issues shows the shared empty-state panel
  3 passed   # the 3 upstream-error-path tests -- pre-existing +error.svelte/+page.ts
             # behavior, so these passed immediately rather than going red naturally

The 3 error-path tests exercise pre-existing behavior (the throw error(...) in each +page.ts, and the shared +error.svelte from #43), so per red-green TDD they needed a one-line mutation to prove the test actually catches a regression, not just a false positive. For each of press/events/newsletter, temporarily replaced if (!res.ok) throw error(...) with if (!res.ok) return { X: [] } (silently swallow the failure instead of erroring), ran just that test, confirmed red, then reverted:

# press (mutated: return { releases: [] } instead of throwing)
1 failed — press › an upstream failure renders the shared error page...
  Error: expect(locator).toBeVisible() failed
  Locator: getByRole('heading', { name: /Couldn.t reach UMass Dining/ })
  Error: element(s) not found

# events (same mutation)
1 failed — events › an upstream failure renders the shared error page
  Error: expect(locator).toBeVisible() failed
  Locator: getByRole('heading', { name: /Couldn.t reach UMass Dining/ })
  Error: element(s) not found

# newsletter (same mutation)
1 failed — newsletter › an upstream failure renders the shared error page
  Error: expect(locator).toBeVisible() failed
  Locator: getByRole('heading', { name: /Couldn.t reach UMass Dining/ })
  Error: element(s) not found

Reverted all three +page.ts files immediately after each probe (git status confirmed clean before implementing the styling).

Implemented the three +page.svelte files, re-ran the full new spec: 9/9 green. Then the full suite (26 tests across all specs, including the sibling-agent-owned shell.spec.ts, rank.spec.ts, filters.spec.ts, favorites.spec.ts, halls-menu.spec.ts, vertical-slice.spec.ts) on PORT=4304: 26/26 green, confirming no regressions to shell nav, error page, or other screens.

Verification against live data

Ran the dev server (PORT=4304) and curled /press, /events, /newsletter directly — SvelteKit's SSR load hit the real umassdining.com upstream (not mocked for this check) and rendered real press releases, events, and newsletter issues through the new .card/.page-title/.empty-state markup. Confirmed the event-image aspect-ratio fix against a real banner (Summer Hours Banner_0.jpg, downloaded and measured at 1024×432 with PIL) — object-contain was the right call over object-cover for that aspect ratio.

Lanes run (original)

  • PORT=4304 npx playwright test (all 26 specs) — pass
  • pnpm --filter web check — clean, 0 errors/warnings
  • pnpm --filter web build — succeeds (pre-existing adapter-auto "could not detect platform" notice only, unrelated to this diff)
  • Not run: shared/mobile/supabase-* CI jobs — no files under those trees changed, so they're unaffected by this diff.

Not merging

Per dispatch instructions, this PR is left open for review, not merged.


Update (2026-08-19): fixed the one blocking review finding

Defect (reviewer-verified against live data): GET https://www.umassdining.com/uapp/get_press returns image URLs with a literal host of default (e.g. https://default/sites/default/files/press/images.png) for all 10 live releases — present, so {#if release.image} in press/+page.svelte:22 passes, but unresolvable, so this PR's fixed h-16 box (which replaced the old collapse-to-0-height behavior) rendered a visible broken-image glyph on every card. h-16 was the load-bearing utility that made the pre-existing bug visible. GET /uapp/get_beacons_events's featured_image field carries the same risk and shares the same guard pattern in events/+page.svelte.

Fix: normalized at the source, per the review's preferred direction. New sanitizeImageUrl() in shared/src/content.ts blanks any image URL without a plausible hostname (must contain a dot, or be localhost); wired into fetchPressReleases and mapEvent, covering press and events for both web and mobile in one place — mobile calls fetchPressReleases/fetchEvents directly with no proxy in between, so it needed no further change. Web's /press and /events +page.ts load functions apply the same sanitizer a second time on the /api/* JSON response — not redundant paranoia, but necessary for the new e2e case to actually exercise the fix: this suite's own established pattern (see vertical-slice.spec.ts's comment) is that Playwright's page.route only intercepts browser-originated requests, never the SvelteKit server process's own outbound fetch to umassdining.com, so mocking **/api/press** bypasses fetchPressReleases entirely. Without the second pass, a red→green e2e test for this fix would have been structurally impossible to write within this codebase's existing mocking boundary.

Red/green evidence:

Unit, shared/src/content.test.ts (written first, run red against unfixed content.ts):

✖ event mapping blanks a featured_image with an unresolvable 'default' host
  AssertionError: actual 'https://default/sites/default/files/events/banner.jpg' !== expected ''
✖ fetchPressReleases blanks an image with an unresolvable 'default' host
  AssertionError: actual 'https://default/sites/default/files/press/images.png' !== expected ''

After adding sanitizeImageUrl and wiring it into fetchPressReleases/mapEvent: pnpm --filter @udine/shared test → 49/49 pass (was 47/49 before the fix, i.e. the 2 new tests, no other regressions).

E2E, web/e2e/content-screens.spec.ts (written first, run red against unfixed +page.ts):

1) press › a present but unresolvable image host renders no visible broken-image box
   Error: expect(locator).toHaveCount(0) failed — Received: 1  (an <img> rendered)
1) events › a present but unresolvable image host renders no visible broken-image box
   Error: expect(locator).toHaveCount(0) failed — Received: 1

After sanitizing in press/+page.ts and events/+page.ts: PORT=4304 npx playwright test (full suite) → 37/37 pass.

Reviewer nit taken: the three no releases/no events/no issues empty-state specs also asserted main li.card count 0 — vacuous, since it'd pass equally whether the card markup is correct or entirely broken (there's no data to render either way once the empty-state check already confirms the right panel showed). Removed those three assertions; .empty-state visibility is the assertion that actually matters.

Lanes run for this fix:

Files changed for this fix: shared/src/content.ts, shared/src/content.test.ts, web/src/routes/press/+page.ts, web/src/routes/events/+page.ts, web/e2e/content-screens.spec.ts. No +page.svelte changes needed — the guards were already correct, they just needed non-empty-but-invalid data to never reach them.

Merged origin/main into this branch first (#49, #57, #58, #59, #61 had landed since) — no conflicts, disjoint files. Still not merging this PR, left open for review per dispatch instructions.

TSVRN9 added 3 commits August 19, 2026 09:59
Restyles the three surviving content screens (FAQ/Staff cut in #50/#51,
not touched) with #43's shared component classes -- .page-title,
.label-rule, .card, .empty-state, .badge -- sharing one list/card
pattern instead of bare <ul><li>. Events' featured-image box uses
object-contain + a wider box (banners are ~1024x432, not square like
press logos) so a fixed square object-cover crop doesn't slice the
banner. New e2e/content-screens.spec.ts covers rendering, graceful
image degradation, empty states, and the shared +error.svelte path for
all three screens.
…solvable "default" image host

Reviewer-verified: GET /uapp/get_press returns image URLs with a literal host of `default`
(e.g. https://default/sites/default/files/press/images.png) for all releases -- present, so
`{#if image}` passes, but unresolvable, so PR #60's fixed h-16 box now renders a visible
broken-image glyph on every card (pre-#60 it collapsed to 0 height).

Fix, per review: normalize at the source in shared/src/content.ts. New sanitizeImageUrl()
blanks any image URL without a plausible hostname (must contain a dot, or be localhost);
applied in fetchPressReleases and mapEvent, covering both press and events for web and
mobile in one place. Web's press/events +page.ts load functions apply the same sanitizer a
second time on the /api/* response, since Playwright's page.route (used throughout this
suite's e2e mocks, per vertical-slice.spec.ts's own comment) can only intercept
browser-originated requests, not the SvelteKit server process's own outbound fetch to
umassdining.com -- without this second pass the new e2e case couldn't exercise the fix at all.

Also takes the reviewer's nit: removed the three `main li.card` count-0 assertions in the
empty-state specs (press/events/newsletter) -- vacuous, since they'd pass equally whether
the card markup is correct or entirely broken, given there's no data to render either way.
`.empty-state` visibility is the assertion that actually matters.
@TSVRN9

TSVRN9 commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Fixed the blocking finding: unresolvable https://default/... image host from GET /uapp/get_press (and the same-shaped featured_image risk in GET /uapp/get_beacons_events) rendering a visible broken-image glyph in the new h-16 box.

Normalized at the source in shared/src/content.ts (sanitizeImageUrl, wired into fetchPressReleases and mapEvent) per the preferred direction — fixes web and mobile in one place, mobile needed no further change since it calls fetchPressReleases/fetchEvents directly. Web's press/events +page.ts also apply the same sanitizer on the /api/* response, which turned out to be necessary (not redundant) for the e2e case to actually exercise anything, given this suite's established page.route boundary only intercepts browser-originated requests, never the SvelteKit server's own outbound fetch to umassdining.com.

Red/green at both levels — details, exact failure output, and full lane results are in the updated PR body. Summary: shared unit tests 47→49 (2 new, both red-then-green), e2e content-screens.spec.ts 2 new cases red (broken <img> rendered) then green, full e2e suite 37/37, web check and web build clean.

Also took the nit on the vacuous main li.card count-0 assertions in the three empty-state specs — removed them, .empty-state visibility is the assertion that actually carries signal.

Commit: c2c7d7f. Not merging, left for review.

@TSVRN9
TSVRN9 merged commit 9826386 into main Aug 19, 2026
7 checks passed
@TSVRN9
TSVRN9 deleted the ux/content-screens branch August 19, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web UI: style content screens (press, events, newsletter)

1 participant