PR-2 of t-paliad-115. The unified Fristen + Termine surface now lives at
/events. Old /deadlines and /appointments list URLs 301-redirect to
/events?type=deadline and /events?type=appointment so existing bookmarks
still land on the right view. Detail pages (/deadlines/{id},
/appointments/{id}) stay type-specific.
Backend (Go).
- New `GET /events` route → handleEventsListPage serves dist/events.html.
- `GET /deadlines` → handleDeadlinesListRedirect (301 → /events?type=deadline).
- `GET /appointments` → handleAppointmentsListRedirect (301 → /events?type=appointment).
- /deadlines/new, /deadlines/calendar, /deadlines/{id}, /appointments/new,
/appointments/calendar, /appointments/{id} unchanged — type-specific
detail / form / legacy-calendar surfaces stay where they are.
Frontend.
- build.ts now emits ONE events.html (not events-deadlines /
events-appointments) with defaultType="all" baked in. The page reads
?type=… and ?view=… on hydration, so /events?type=deadline lands on
the Fristen-only Cards view, /events?view=calendar opens the calendar,
and bare /events shows the Beides view.
- Sidebar Fristen / Termine entries point at /events?type=deadline and
/events?type=appointment. The SSR active-state matches exactly via
href === currentPath, so detail/new/calendar pages that pass
currentPath="/events?type=deadline" (resp. appointment) still
highlight the right entry.
- events.ts hydration adds applySidebarTypeHighlight(): on bare /events
the sidebar SSRs with neither entry lit, and we re-highlight the
matching entry whenever the in-page chip toggle changes the active
type. Sidebar stays in sync without a server round-trip.
- Updated every list-target reference: palette-actions.ts (Cmd-K
navigation), deadlines-detail.ts + appointments-detail.ts (post-delete
redirect), and the back-link / cancel hrefs in the *-new + *-detail +
*-calendar TSX templates. Detail-page Sidebar/BottomNav currentPath
also moved from "/deadlines" → "/events?type=deadline" so the new
highlight contract holds end-to-end.
Out of scope (per task brief).
- A third "Ereignisse / Alle Events" sidebar entry pointing at /events
bare. m's call: keep two entries; defer until signal.
- Removing /deadlines/calendar + /appointments/calendar standalone
pages. The new /events?view=calendar covers the same need but the
legacy URLs stay live for one cycle.
Build clean: `cd frontend && bun run build` + `go build/vet/test ./...`.
63 lines
2.5 KiB
Go
63 lines
2.5 KiB
Go
package handlers
|
|
|
|
import "net/http"
|
|
|
|
// Server-rendered page endpoints for the Phase F Appointments UI.
|
|
// HTML is generated at build time by frontend/build.ts; the per-page
|
|
// client TS bundles call /api/appointments* to populate the DOM and read
|
|
// id/project_id from window.location.
|
|
|
|
// handleAppointmentsListRedirect 301-redirects the legacy /appointments
|
|
// list URL to the canonical /events?type=appointment (t-paliad-115).
|
|
// Detail page /appointments/{id} stays type-specific. Drop this redirect
|
|
// once we're confident no caches / bookmarks / external links still hit
|
|
// the old URL.
|
|
func handleAppointmentsListRedirect(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/events?type=appointment", http.StatusMovedPermanently)
|
|
}
|
|
|
|
func handleAppointmentsNewPage(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, "dist/appointments-new.html")
|
|
}
|
|
|
|
func handleAppointmentsDetailPage(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, "dist/appointments-detail.html")
|
|
}
|
|
|
|
func handleAppointmentsCalendarPage(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, "dist/appointments-calendar.html")
|
|
}
|
|
|
|
// handleSettingsPage serves the unified settings page with tabs for
|
|
// Profil / Benachrichtigungen / CalDAV. The active tab is picked
|
|
// client-side from ?tab=<name> so switching tabs doesn't round-trip.
|
|
func handleSettingsPage(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, "dist/settings.html")
|
|
}
|
|
|
|
// settingsTabAliases maps every supported /settings/<slug> deep-link to its
|
|
// canonical ?tab=<name> value the client TS understands. Both the German tab
|
|
// IDs (profil/benachrichtigungen) and intuitive English aliases
|
|
// (profile/notifications) are accepted so bookmarks, smoke tests, and
|
|
// manually-typed URLs all land on the right tab.
|
|
var settingsTabAliases = map[string]string{
|
|
"profil": "profil",
|
|
"profile": "profil",
|
|
"benachrichtigungen": "benachrichtigungen",
|
|
"notifications": "benachrichtigungen",
|
|
"caldav": "caldav",
|
|
}
|
|
|
|
// handleSettingsTabRedirect turns /settings/<slug> into /settings?tab=<canonical>
|
|
// as 301 Moved Permanently. Unknown slugs fall back to the bare /settings page
|
|
// (the client picks the default tab) so a typo doesn't 404.
|
|
func handleSettingsTabRedirect(w http.ResponseWriter, r *http.Request) {
|
|
slug := r.PathValue("tab")
|
|
canonical, ok := settingsTabAliases[slug]
|
|
if !ok {
|
|
http.Redirect(w, r, "/settings", http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/settings?tab="+canonical, http.StatusMovedPermanently)
|
|
}
|