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.
62 lines
2.0 KiB
Go
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")
|
|
}
|