From a13e215e316f900f38eeb8128ff666933ffa00b0 Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Fri, 9 Jan 2026 17:01:47 +0100 Subject: [PATCH] Add multi-page app example demonstrating navigation Add a comprehensive example showing client-side routing: - Router setup with multiple routes (/, /about, /users, /admin) - Route parameters (/users/:id) with the NumericIdGuard - Navigation using Link component and programmatic Navigate/Back - Route guards with AuthGuard for protected admin page - Browser history integration with back/forward support Closes #6 Co-Authored-By: Claude Opus 4.5 --- examples/multipage/main.go | 265 +++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 examples/multipage/main.go diff --git a/examples/multipage/main.go b/examples/multipage/main.go new file mode 100644 index 0000000..ef840ce --- /dev/null +++ b/examples/multipage/main.go @@ -0,0 +1,265 @@ +//go:build js && wasm + +// Package main demonstrates Iris client-side routing capabilities. +// +// This example shows: +// - Router setup with multiple routes +// - Route parameters (/users/:id) +// - Navigation between pages using Link and Navigate +// - Route guards for protected routes +// - History management (back/forward) +package main + +import ( + "fmt" + + "git.flowmade.one/flowmade-one/iris/navigation" + "git.flowmade.one/flowmade-one/iris/reactive" + "git.flowmade.one/flowmade-one/iris/ui" +) + +// Global auth state to demonstrate route guards +var isAuthenticated = reactive.NewSignal(false) + +func main() { + // Define routes with their handlers and guards + routes := []navigation.Route{ + {Path: "/", Handler: homePage}, + {Path: "/about", Handler: aboutPage}, + {Path: "/users", Handler: usersPage}, + {Path: "/users/:id", Handler: userDetailPage, Guards: []navigation.RouteGuard{ + navigation.NumericIdGuard(), + }}, + {Path: "/admin", Handler: adminPage, Guards: []navigation.RouteGuard{ + navigation.AuthGuard(func() bool { return isAuthenticated.Get() }), + }}, + } + + // Create router and set up navigation + router := navigation.NewRouter(routes) + router.SetNotFoundHandler(notFoundPage) + navigation.SetGlobalRouter(router) + + // Mount the app with router support + ui.NewAppWithRouter(router) + + // Keep the application running + select {} +} + +// homePage renders the landing page with navigation links +func homePage(params map[string]string) ui.View { + return pageLayout("Home", + ui.VerticalGroup( + ui.TextFromString("Welcome to the Iris Multi-Page Demo"). + Color("#333"). + Padding("16px"), + ui.TextFromString("This example demonstrates client-side routing with:"). + Color("#666"). + Padding("8px"), + ui.VerticalGroup( + ui.TextFromString("- Multiple routes and page navigation").Color("#888"), + ui.TextFromString("- Route parameters (see /users/:id)").Color("#888"), + ui.TextFromString("- Route guards for protected pages").Color("#888"), + ui.TextFromString("- Browser history integration").Color("#888"), + ).Padding("8px 24px"), + ui.TextFromString("Use the navigation bar above to explore."). + Color("#666"). + Padding("16px"), + ), + ) +} + +// aboutPage renders information about the demo +func aboutPage(params map[string]string) ui.View { + return pageLayout("About", + ui.VerticalGroup( + ui.TextFromString("About Iris Navigation"). + Color("#333"). + Padding("16px"), + ui.TextFromString("The navigation package provides:"). + Color("#666"). + Padding("8px"), + ui.VerticalGroup( + ui.TextFromString("Router - Define routes with path patterns and handlers").Color("#888"), + ui.TextFromString("RouteGuard - Protect routes with custom logic").Color("#888"), + ui.TextFromString("HistoryManager - Integrate with browser history").Color("#888"), + ui.TextFromString("Link - Create navigational elements").Color("#888"), + ui.TextFromString("Navigate/Back/Forward - Programmatic navigation").Color("#888"), + ).Padding("8px 24px"), + ), + ) +} + +// usersPage renders a list of users with links to their detail pages +func usersPage(params map[string]string) ui.View { + users := []struct { + ID string + Name string + }{ + {"1", "Alice"}, + {"2", "Bob"}, + {"3", "Charlie"}, + {"4", "Diana"}, + } + + var userLinks []ui.View + for _, user := range users { + // Capture user in closure + u := user + userLinks = append(userLinks, + navigation.Link(fmt.Sprintf("/users/%s", u.ID), + ui.TextFromString(fmt.Sprintf("View %s (ID: %s)", u.Name, u.ID)). + Color("#0066cc"), + ).Padding("8px").Cursor("pointer"), + ) + } + + return pageLayout("Users", + ui.VerticalGroup( + ui.TextFromString("User Directory"). + Color("#333"). + Padding("16px"), + ui.TextFromString("Click a user to see their details:"). + Color("#666"). + Padding("8px"), + ui.VerticalGroup(userLinks...).Padding("8px"), + ), + ) +} + +// userDetailPage shows details for a specific user using the :id parameter +func userDetailPage(params map[string]string) ui.View { + userID := params["id"] + + // Simulated user data + userData := map[string]struct { + Name string + Email string + Role string + }{ + "1": {"Alice", "alice@example.com", "Admin"}, + "2": {"Bob", "bob@example.com", "User"}, + "3": {"Charlie", "charlie@example.com", "User"}, + "4": {"Diana", "diana@example.com", "Moderator"}, + } + + user, exists := userData[userID] + if !exists { + return pageLayout("User Not Found", + ui.VerticalGroup( + ui.TextFromString(fmt.Sprintf("User with ID %s not found", userID)). + Color("#ff4444"). + Padding("16px"), + ui.Button(func() { + navigation.Navigate("/users") + }, ui.TextFromString("Back to Users")). + Padding("8px"), + ), + ) + } + + return pageLayout(fmt.Sprintf("User: %s", user.Name), + ui.VerticalGroup( + ui.TextFromString(fmt.Sprintf("User Details (ID: %s)", userID)). + Color("#333"). + Padding("16px"), + ui.VerticalGroup( + ui.TextFromString(fmt.Sprintf("Name: %s", user.Name)).Color("#666"), + ui.TextFromString(fmt.Sprintf("Email: %s", user.Email)).Color("#666"), + ui.TextFromString(fmt.Sprintf("Role: %s", user.Role)).Color("#666"), + ).Padding("8px 24px"), + ui.HorizontalGroup( + ui.Button(func() { + navigation.Back() + }, ui.TextFromString("Go Back")).Padding("8px"), + ui.Button(func() { + navigation.Navigate("/users") + }, ui.TextFromString("All Users")).Padding("8px"), + ).Padding("16px"), + ), + ) +} + +// adminPage is a protected route that requires authentication +func adminPage(params map[string]string) ui.View { + return pageLayout("Admin", + ui.VerticalGroup( + ui.TextFromString("Admin Dashboard"). + Color("#333"). + Padding("16px"), + ui.TextFromString("Welcome to the protected admin area!"). + Color("#28a745"). + Padding("8px"), + ui.TextFromString("This page is protected by an AuthGuard."). + Color("#666"). + Padding("8px"), + ui.Button(func() { + isAuthenticated.Set(false) + navigation.Navigate("/") + }, ui.TextFromString("Logout")). + Padding("8px"). + Background("#dc3545"). + Foreground("#fff"), + ), + ) +} + +// notFoundPage renders when no route matches +func notFoundPage() ui.View { + return pageLayout("404", + ui.VerticalGroup( + ui.TextFromString("404 - Page Not Found"). + Color("#ff4444"). + Padding("16px"), + ui.TextFromString("The page you are looking for does not exist."). + Color("#666"). + Padding("8px"), + ui.Button(func() { + navigation.Navigate("/") + }, ui.TextFromString("Go Home")).Padding("8px"), + ), + ) +} + +// pageLayout provides consistent page structure with navigation +func pageLayout(title string, content ui.View) ui.View { + return ui.VerticalGroup( + navBar(), + ui.VerticalGroup( + ui.TextFromString(title). + Color("#333"). + Padding("8px"). + Background("#f0f0f0"). + Width("100%"), + content, + ).Padding("16px"), + ).MinHeight("100vh") +} + +// navBar creates the navigation header with links +func navBar() ui.View { + return ui.HorizontalGroup( + navigation.Link("/", ui.TextFromString("Home").Color("#fff")), + navigation.Link("/about", ui.TextFromString("About").Color("#fff")), + navigation.Link("/users", ui.TextFromString("Users").Color("#fff")), + authButton(), + ).Background("#333").Padding("8px 16px").AlignItems("center") +} + +// authButton shows login/admin based on auth state +func authButton() ui.View { + return ui.Button(func() { + if isAuthenticated.Get() { + navigation.Navigate("/admin") + } else { + isAuthenticated.Set(true) + navigation.Navigate("/admin") + } + }, ui.TextFromFunction(func() string { + if isAuthenticated.Get() { + return "Admin" + } + return "Login" + }).Color("#fff")).Background("transparent").Border("1px solid #fff").Padding("4px 12px").Cursor("pointer") +}