From 79f994e6e8e48928886c5989c3f24fb2c44839d4 Mon Sep 17 00:00:00 2001 From: Hugo Nijhuis Date: Fri, 9 Jan 2026 17:03:13 +0100 Subject: [PATCH] Add dashboard example showing realistic admin panel This example demonstrates all key Iris features for internal tool teams: - Multiple pages with client-side routing - Auth-protected sections with route guards - Reactive data display with signals - Form inputs (text, checkbox, slider, select) - Component composition with reusable layouts Test credentials: admin/admin (full access) or user/user (limited access) Closes #8 Co-Authored-By: Claude Opus 4.5 --- examples/dashboard/main.go | 669 +++++++++++++++++++++++++++++++++++++ 1 file changed, 669 insertions(+) create mode 100644 examples/dashboard/main.go diff --git a/examples/dashboard/main.go b/examples/dashboard/main.go new file mode 100644 index 0000000..fae9742 --- /dev/null +++ b/examples/dashboard/main.go @@ -0,0 +1,669 @@ +//go:build js && wasm + +// Dashboard example demonstrating a realistic internal tool admin panel. +// This example showcases: +// - Multiple pages with routing +// - Auth-protected sections +// - Reactive data display +// - Form inputs +// - Component composition +package main + +import ( + "fmt" + "strconv" + + "git.flowmade.one/flowmade-one/iris/navigation" + "git.flowmade.one/flowmade-one/iris/reactive" + "git.flowmade.one/flowmade-one/iris/ui" +) + +// Application state - simulated auth and data +var ( + isAuthenticated = reactive.NewSignal(false) + currentUser = reactive.NewSignal("") + userRole = reactive.NewSignal("guest") +) + +// Simulated dashboard metrics +var ( + totalUsers = reactive.NewSignal(1247) + activeOrders = reactive.NewSignal(89) + revenue = reactive.NewSignal(45230.50) + pendingTasks = reactive.NewSignal(12) +) + +// Simulated user list +type User struct { + ID int + Name string + Email string + Role string + Active bool +} + +var users = reactive.NewSignal([]User{ + {1, "Alice Johnson", "alice@example.com", "admin", true}, + {2, "Bob Smith", "bob@example.com", "user", true}, + {3, "Carol White", "carol@example.com", "user", false}, + {4, "Dave Brown", "dave@example.com", "moderator", true}, +}) + +// Settings state +var ( + darkMode = reactive.NewSignal(false) + emailNotifications = reactive.NewSignal(true) + apiEndpoint = reactive.NewSignal("https://api.example.com") + refreshInterval = reactive.NewSignal(30) +) + +func main() { + // Define routes with guards + routes := []navigation.Route{ + {Path: "/", Handler: func(params map[string]string) ui.View { return homeView() }}, + {Path: "/login", Handler: func(params map[string]string) ui.View { return loginView() }}, + {Path: "/users", Handler: func(params map[string]string) ui.View { return usersView() }, Guards: []navigation.RouteGuard{authGuard}}, + {Path: "/users/:id", Handler: userDetailView, Guards: []navigation.RouteGuard{authGuard}}, + {Path: "/settings", Handler: func(params map[string]string) ui.View { return settingsView() }, Guards: []navigation.RouteGuard{authGuard}}, + {Path: "/admin", Handler: func(params map[string]string) ui.View { return adminView() }, Guards: []navigation.RouteGuard{authGuard, adminGuard}}, + } + + router := navigation.NewRouter(routes) + router.SetNotFoundHandler(notFoundView) + navigation.SetGlobalRouter(router) + + // Create the main app layout + app := dashboardLayout() + ui.NewApp(app) + + select {} +} + +// Auth guard - checks if user is authenticated +func authGuard(route *navigation.Route, params map[string]string) bool { + if !isAuthenticated.Get() { + navigation.Navigate("/login") + return false + } + return true +} + +// Admin guard - checks if user has admin role +func adminGuard(route *navigation.Route, params map[string]string) bool { + return userRole.Get() == "admin" +} + +// Main dashboard layout with sidebar and content area +func dashboardLayout() ui.View { + sidebar := createSidebar() + content := createContentArea() + + return ui.FlexLayout( + []string{"240px", "1fr"}, + sidebar, + content, + ).MinHeight("100vh") +} + +// Sidebar with navigation links +func createSidebar() ui.View { + return ui.VerticalGroup( + // Logo/Brand + ui.TextFromString("Iris Dashboard"). + Font(ui.NewFont().Size("24px").Weight("bold")). + Padding("20px"). + Color("#fff"), + + ui.Divider().Background("#444"), + + // Navigation links + navLink("/", "Home"), + navLink("/users", "Users"), + navLink("/settings", "Settings"), + navLink("/admin", "Admin"), + + ui.Spacer(), + + // User info at bottom + ui.VerticalGroup( + ui.Divider().Background("#444"), + ui.TextFromFunction(func() string { + if isAuthenticated.Get() { + return "Logged in as: " + currentUser.Get() + } + return "Not logged in" + }).Color("#aaa").Padding("10px"), + loginLogoutButton(), + ), + ).Background("#1a1a2e").Width("240px").MinHeight("100vh").Padding("0") +} + +func navLink(path, label string) ui.View { + return navigation.Link(path, + ui.TextFromString(label).Color("#ccc"), + ).Padding("12px 20px").Cursor("pointer") +} + +func loginLogoutButton() ui.View { + return ui.Button(func() { + if isAuthenticated.Get() { + isAuthenticated.Set(false) + currentUser.Set("") + userRole.Set("guest") + navigation.Navigate("/") + } else { + navigation.Navigate("/login") + } + }, ui.TextFromFunction(func() string { + if isAuthenticated.Get() { + return "Logout" + } + return "Login" + }).Color("#fff")). + Background("#e94560"). + Padding("10px 20px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer"). + Margin("10px") +} + +// Content area that shows the current route view +func createContentArea() ui.View { + view := ui.NewView() + + reactive.NewEffect(func() { + router := navigation.GetGlobalRouter() + if router != nil { + currentView := router.GetCurrentView() + view.Element().Set("innerHTML", "") + view.Child(currentView) + } + }) + + return view.Background("#f0f0f0").Padding("20px").Overflow("auto") +} + +// Home view - dashboard overview +func homeView() ui.View { + return ui.VerticalGroup( + ui.TextFromString("Dashboard Overview"). + Font(ui.NewFont().Size("28px").Weight("bold")). + Color("#333"), + + // Metrics cards + ui.HorizontalGroup( + metricCard("Total Users", func() string { return strconv.Itoa(totalUsers.Get()) }, "#4ecca3"), + metricCard("Active Orders", func() string { return strconv.Itoa(activeOrders.Get()) }, "#e94560"), + metricCard("Revenue", func() string { return fmt.Sprintf("$%.2f", revenue.Get()) }, "#16c79a"), + metricCard("Pending Tasks", func() string { return strconv.Itoa(pendingTasks.Get()) }, "#f39c12"), + ).Gap("20px").Margin("20px 0"), + + // Quick actions + ui.TextFromString("Quick Actions"). + Font(ui.NewFont().Size("20px").Weight("bold")). + Color("#333"). + Margin("20px 0 10px 0"), + + ui.HorizontalGroup( + actionButton("Add User", func() { navigation.Navigate("/users") }), + actionButton("View Reports", func() { /* simulated action */ }), + actionButton("Export Data", func() { /* simulated action */ }), + ).Gap("10px"), + + // Recent activity (simulated) + ui.TextFromString("Recent Activity"). + Font(ui.NewFont().Size("20px").Weight("bold")). + Color("#333"). + Margin("30px 0 10px 0"), + + activityItem("User Alice updated profile", "2 minutes ago"), + activityItem("New order #1234 received", "15 minutes ago"), + activityItem("System backup completed", "1 hour ago"), + ).AlignLeft().Gap("5px") +} + +func metricCard(title string, valueFn func() string, color string) ui.View { + return ui.VerticalGroup( + ui.TextFromString(title).Color("#666").Font(ui.NewFont().Size("14px")), + ui.TextFromFunction(valueFn). + Font(ui.NewFont().Size("32px").Weight("bold")). + Color(color), + ).Background("#fff"). + Padding("20px"). + BorderRadius("8px"). + BoxShadow("0 2px 4px rgba(0,0,0,0.1)"). + Width("200px"). + AlignCenter() +} + +func actionButton(label string, action func()) ui.View { + return ui.Button(action, + ui.TextFromString(label).Color("#fff"), + ).Background("#0077b6"). + Padding("12px 24px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer") +} + +func activityItem(text, time string) ui.View { + return ui.HorizontalGroup( + ui.TextFromString(text).Color("#333"), + ui.Spacer(), + ui.TextFromString(time).Color("#999").Font(ui.NewFont().Size("12px")), + ).Background("#fff"). + Padding("15px"). + BorderRadius("4px"). + Margin("5px 0") +} + +// Login view +func loginView() ui.View { + username := reactive.NewSignal("") + password := reactive.NewSignal("") + errorMsg := reactive.NewSignal("") + + return ui.VerticalGroup( + ui.TextFromString("Login"). + Font(ui.NewFont().Size("28px").Weight("bold")). + Color("#333"), + + ui.VerticalGroup( + ui.TextFromString("Username").Color("#666").Margin("0 0 5px 0"), + ui.TextInput(&username, "Enter username"). + Padding("10px"). + Border("1px solid #ddd"). + BorderRadius("4px"). + Width("300px"), + + ui.TextFromString("Password").Color("#666").Margin("15px 0 5px 0"), + ui.TextInput(&password, "Enter password"). + Padding("10px"). + Border("1px solid #ddd"). + BorderRadius("4px"). + Width("300px"), + + ui.TextFromFunction(func() string { + return errorMsg.Get() + }).Color("#e94560").Margin("10px 0"), + + ui.Button(func() { + // Simulated authentication + u := username.Get() + p := password.Get() + if u == "admin" && p == "admin" { + isAuthenticated.Set(true) + currentUser.Set("admin") + userRole.Set("admin") + navigation.Navigate("/") + } else if u == "user" && p == "user" { + isAuthenticated.Set(true) + currentUser.Set("user") + userRole.Set("user") + navigation.Navigate("/") + } else { + errorMsg.Set("Invalid credentials. Try admin/admin or user/user") + } + }, ui.TextFromString("Login").Color("#fff")). + Background("#0077b6"). + Padding("12px 40px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer"). + Margin("20px 0"), + ).Background("#fff"). + Padding("30px"). + BorderRadius("8px"). + BoxShadow("0 2px 10px rgba(0,0,0,0.1)"). + Gap("5px"), + ).AlignCenter().Padding("50px") +} + +// Users view - list and manage users +func usersView() ui.View { + searchTerm := reactive.NewSignal("") + + return ui.VerticalGroup( + ui.TextFromString("User Management"). + Font(ui.NewFont().Size("28px").Weight("bold")). + Color("#333"), + + // Search bar + ui.HorizontalGroup( + ui.TextInput(&searchTerm, "Search users..."). + Padding("10px"). + Border("1px solid #ddd"). + BorderRadius("4px"). + Width("300px"), + ui.Button(func() { + // Add new user simulation + currentUsers := users.Get() + newID := len(currentUsers) + 1 + currentUsers = append(currentUsers, User{ + ID: newID, + Name: fmt.Sprintf("New User %d", newID), + Email: fmt.Sprintf("user%d@example.com", newID), + Role: "user", + Active: true, + }) + users.Set(currentUsers) + totalUsers.Set(totalUsers.Get() + 1) + }, ui.TextFromString("Add User").Color("#fff")). + Background("#4ecca3"). + Padding("10px 20px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer"), + ).Gap("10px").Margin("20px 0"), + + // User list + ui.VerticalGroup( + userListHeader(), + userList(&searchTerm), + ).Background("#fff"). + BorderRadius("8px"). + BoxShadow("0 2px 4px rgba(0,0,0,0.1)"), + ).AlignLeft().Gap("10px") +} + +func userListHeader() ui.View { + return ui.FlexLayout( + []string{"60px", "1fr", "1fr", "100px", "100px"}, + ui.TextFromString("ID").Font(ui.NewFont().Weight("bold")).Color("#666"), + ui.TextFromString("Name").Font(ui.NewFont().Weight("bold")).Color("#666"), + ui.TextFromString("Email").Font(ui.NewFont().Weight("bold")).Color("#666"), + ui.TextFromString("Role").Font(ui.NewFont().Weight("bold")).Color("#666"), + ui.TextFromString("Status").Font(ui.NewFont().Weight("bold")).Color("#666"), + ).Padding("15px").BorderBottom("2px solid #eee") +} + +func userList(searchTerm *reactive.Signal[string]) ui.View { + view := ui.NewView() + + reactive.NewEffect(func() { + view.Element().Set("innerHTML", "") + search := searchTerm.Get() + for _, user := range users.Get() { + // Simple search filter + if search != "" && !containsIgnoreCase(user.Name, search) && !containsIgnoreCase(user.Email, search) { + continue + } + view.Child(userRow(user)) + } + }) + + return view +} + +func userRow(user User) ui.View { + statusColor := "#4ecca3" + statusText := "Active" + if !user.Active { + statusColor = "#e94560" + statusText = "Inactive" + } + + return ui.FlexLayout( + []string{"60px", "1fr", "1fr", "100px", "100px"}, + ui.TextFromString(strconv.Itoa(user.ID)).Color("#333"), + navigation.Link( + fmt.Sprintf("/users/%d", user.ID), + ui.TextFromString(user.Name).Color("#0077b6"), + ), + ui.TextFromString(user.Email).Color("#666"), + ui.TextFromString(user.Role).Color("#333"), + ui.TextFromString(statusText).Color(statusColor), + ).Padding("15px").BorderBottom("1px solid #eee") +} + +// User detail view +func userDetailView(params map[string]string) ui.View { + userID := params["id"] + id, _ := strconv.Atoi(userID) + + var user *User + for _, u := range users.Get() { + if u.ID == id { + userCopy := u + user = &userCopy + break + } + } + + if user == nil { + return ui.TextFromString("User not found").Color("#e94560") + } + + return ui.VerticalGroup( + ui.Button(func() { + navigation.Navigate("/users") + }, ui.TextFromString("Back to Users").Color("#0077b6")). + Background("transparent"). + Border("none"). + Cursor("pointer"). + Padding("0"). + Margin("0 0 20px 0"), + + ui.TextFromString(user.Name). + Font(ui.NewFont().Size("28px").Weight("bold")). + Color("#333"), + + ui.VerticalGroup( + detailRow("ID", strconv.Itoa(user.ID)), + detailRow("Email", user.Email), + detailRow("Role", user.Role), + detailRow("Status", map[bool]string{true: "Active", false: "Inactive"}[user.Active]), + ).Background("#fff"). + Padding("20px"). + BorderRadius("8px"). + BoxShadow("0 2px 4px rgba(0,0,0,0.1)"). + Gap("10px"). + Margin("20px 0"), + ).AlignLeft() +} + +func detailRow(label, value string) ui.View { + return ui.HorizontalGroup( + ui.TextFromString(label+":").Font(ui.NewFont().Weight("bold")).Color("#666").Width("100px"), + ui.TextFromString(value).Color("#333"), + ).Gap("10px") +} + +// Settings view +func settingsView() ui.View { + return ui.VerticalGroup( + ui.TextFromString("Settings"). + Font(ui.NewFont().Size("28px").Weight("bold")). + Color("#333"), + + // Appearance section + settingsSection("Appearance", + ui.Checkbox(&darkMode, "Enable Dark Mode"), + ), + + // Notifications section + settingsSection("Notifications", + ui.Checkbox(&emailNotifications, "Email Notifications"), + ), + + // API Settings section + settingsSection("API Configuration", + ui.VerticalGroup( + ui.TextFromString("API Endpoint").Color("#666").Margin("0 0 5px 0"), + ui.TextInput(&apiEndpoint, "API URL"). + Padding("10px"). + Border("1px solid #ddd"). + BorderRadius("4px"). + Width("400px"), + + ui.TextFromString("Refresh Interval (seconds)").Color("#666").Margin("15px 0 5px 0"), + ui.HorizontalGroup( + ui.Slider(&refreshInterval, 5, 120), + ui.TextFromFunction(func() string { + return fmt.Sprintf("%ds", refreshInterval.Get()) + }).Color("#333").Width("50px"), + ).Gap("10px").AlignCenter(), + ).Gap("5px"), + ), + + // Current settings display + ui.VerticalGroup( + ui.TextFromString("Current Configuration"). + Font(ui.NewFont().Size("18px").Weight("bold")). + Color("#333"), + ui.TextFromFunction(func() string { + return fmt.Sprintf("Dark Mode: %v | Notifications: %v | Refresh: %ds", + darkMode.Get(), emailNotifications.Get(), refreshInterval.Get()) + }).Color("#666").Font(ui.NewFont().Size("14px")), + ).Background("#f8f8f8"). + Padding("15px"). + BorderRadius("4px"). + Margin("20px 0"), + + // Save button + ui.Button(func() { + // Simulated save action + fmt.Println("Settings saved!") + }, ui.TextFromString("Save Settings").Color("#fff")). + Background("#0077b6"). + Padding("12px 30px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer"), + ).AlignLeft().Gap("10px").Padding("0 0 20px 0") +} + +func settingsSection(title string, content ui.View) ui.View { + return ui.VerticalGroup( + ui.TextFromString(title). + Font(ui.NewFont().Size("18px").Weight("bold")). + Color("#333"), + content, + ).Background("#fff"). + Padding("20px"). + BorderRadius("8px"). + BoxShadow("0 2px 4px rgba(0,0,0,0.1)"). + Gap("15px"). + Margin("20px 0") +} + +// Admin view - protected section +func adminView() ui.View { + return ui.VerticalGroup( + ui.TextFromString("Admin Panel"). + Font(ui.NewFont().Size("28px").Weight("bold")). + Color("#333"), + + ui.TextFromString("This section is only accessible to administrators."). + Color("#666"). + Margin("10px 0"), + + // Admin actions + ui.VerticalGroup( + ui.TextFromString("System Actions"). + Font(ui.NewFont().Size("18px").Weight("bold")). + Color("#333"), + + ui.HorizontalGroup( + adminActionButton("Clear Cache", func() { + fmt.Println("Cache cleared!") + }), + adminActionButton("Restart Services", func() { + fmt.Println("Services restarted!") + }), + adminActionButton("Run Backup", func() { + fmt.Println("Backup started!") + }), + ).Gap("10px"), + ).Background("#fff"). + Padding("20px"). + BorderRadius("8px"). + BoxShadow("0 2px 4px rgba(0,0,0,0.1)"). + Gap("15px"). + Margin("20px 0"), + + // Danger zone + ui.VerticalGroup( + ui.TextFromString("Danger Zone"). + Font(ui.NewFont().Size("18px").Weight("bold")). + Color("#e94560"), + + ui.TextFromString("These actions are irreversible. Use with caution."). + Color("#999"). + Font(ui.NewFont().Size("14px")), + + ui.Button(func() { + fmt.Println("Reset initiated!") + }, ui.TextFromString("Reset All Data").Color("#fff")). + Background("#e94560"). + Padding("10px 20px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer"), + ).Background("#fff"). + Padding("20px"). + BorderRadius("8px"). + Border("2px solid #e94560"). + Gap("10px"). + Margin("20px 0"), + ).AlignLeft().Gap("10px") +} + +func adminActionButton(label string, action func()) ui.View { + return ui.Button(action, + ui.TextFromString(label).Color("#fff"), + ).Background("#6c757d"). + Padding("10px 20px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer") +} + +// 404 Not Found view +func notFoundView() ui.View { + return ui.VerticalGroup( + ui.TextFromString("404"). + Font(ui.NewFont().Size("72px").Weight("bold")). + Color("#e94560"), + ui.TextFromString("Page Not Found"). + Font(ui.NewFont().Size("24px")). + Color("#666"), + ui.Button(func() { + navigation.Navigate("/") + }, ui.TextFromString("Go Home").Color("#fff")). + Background("#0077b6"). + Padding("12px 30px"). + Border("none"). + BorderRadius("4px"). + Cursor("pointer"). + Margin("20px 0"), + ).AlignCenter().Padding("50px") +} + +// Helper function for case-insensitive search +func containsIgnoreCase(str, substr string) bool { + return len(str) >= len(substr) && contains(toLower(str), toLower(substr)) +} + +func toLower(s string) string { + result := make([]byte, len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + if c >= 'A' && c <= 'Z' { + result[i] = c + 32 + } else { + result[i] = c + } + } + return string(result) +} + +func contains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} -- 2.49.1