Files
paliad/internal/handlers/views_pages.go
m fdde9eb754 feat(t-paliad-144 A2): frontend Custom Views UI
Phase A2 of the data-display-model rethink. Builds on A1's API contract
(merged as cda4b40). User-visible.

What lands:

- TSX shells for /views (the view runner) and /views/new + /views/{slug}/edit
  (the editor). One TSX per page; client/views.ts + views-editor.ts
  hydrate.

- Three render-shape components in client/views/: shape-list.ts (table
  for density=comfortable, compact one-line stream for density=compact —
  the activity-feed look without a separate "activity" shape per Q4 lock-
  in 2026-05-07), shape-cards.ts (day-grouped chronological), and
  shape-calendar.ts (month grid with day-pills, mobile cards-fallback
  notice on viewports <600px per design §9 trade-off 8).

- Generic view shell that resolves a slug to a system view (via
  /api/views/system) or a user view (via /api/user-views), runs it via
  POST /api/views/{slug}/run, dispatches to the matching shape, exposes
  a 3-button shape switcher that swaps the live render without re-fetching,
  and surfaces the inaccessible-projects toast when the substrate flags
  some IDs (Q17 fail-open attribution).

- View editor with widgets for name/slug/icon, sources (4 checkboxes),
  scope mode (all_visible / my_subtree / personal_only), time horizon
  (six fixed options), shape, and list density. Slug regex enforced
  client-side mirroring the server validator. Save → POST/PATCH; delete
  → simple yes/no confirm (Q25 lock-in).

- Sidebar "Meine Sichten" group between Arbeit and Werkzeuge. Renders
  empty server-side; client/sidebar.ts.initUserViewsGroup() hydrates from
  GET /api/user-views on mount, injecting one nav item per saved view
  + an always-present "+ Neue Sicht" trailing entry. show_count=true
  views get a sidebar badge updated by a fire-and-forget run query.

- Page handlers /views (most-recently-used redirect or onboarding shell),
  /views/{slug}, /views/new, /views/{slug}/edit. All gateOnboarded.

- 91 new i18n keys (DE+EN) covering nav.group.user_views, view shell,
  shape labels, source/kind/horizon/scope vocabulary, editor form,
  empty/error/onboarding states.

- ~250 lines of CSS for the views shell, list/cards/calendar shapes,
  Meine Sichten sidebar group.

- build.ts registers views.tsx + views-editor.tsx page renderers and
  the two client bundles.

Frontend builds clean (i18n codegen 1700→1791 keys), backend builds +
vets clean, all tests pass, IIFE wrap intact on the new bundles.
2026-05-07 13:15:55 +02:00

62 lines
2.0 KiB
Go

package handlers
// Page handlers for the Custom Views shell (t-paliad-144 Phase A2).
//
// Three URLs:
// GET /views — landing; redirects to most-recently-used
// saved view, or shows the empty/onboarding
// card.
// GET /views/{slug} — render a saved or system view.
// GET /views/new — view editor (blank slate).
// GET /views/{slug}/edit — view editor (edit existing).
//
// Each route serves the static dist HTML; the client bundle (views.ts /
// views-editor.ts) hydrates via /api/* on load.
import (
"net/http"
)
// GET /views — landing.
//
// Behaviour matches design Q10 most-recently-used:
// - If the caller has a saved view with last_used_at set → 302 to it.
// - Otherwise serve the onboarding shell (the views.html dist file
// handles the empty state in JS).
func handleViewsLandingPage(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
if dbSvc.userView != nil {
mr, err := dbSvc.userView.MostRecent(r.Context(), uid)
if err == nil && mr != nil {
http.Redirect(w, r, "/views/"+mr.Slug, http.StatusFound)
return
}
}
http.ServeFile(w, r, "dist/views.html")
}
// GET /views/{slug} — saved or system view shell.
//
// The handler doesn't validate the slug here — the client bundle calls
// POST /api/views/{slug}/run and lets the API surface the 404 with a
// proper empty-state. This keeps the page surface trivially cacheable.
func handleViewsShellPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/views.html")
}
// GET /views/new — editor with a blank slate.
func handleViewsNewPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/views-editor.html")
}
// GET /views/{slug}/edit — editor for an existing saved view.
func handleViewsEditPage(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "dist/views-editor.html")
}