package navigation import ( "strings" "git.flowmade.one/flowmade-one/iris/reactive" "git.flowmade.one/flowmade-one/iris/ui" ) type RouteGuard func(route *Route, params map[string]string) bool type Route struct { Path string Handler func(params map[string]string) ui.View Guards []RouteGuard parts []string params []string } type Router struct { routes []Route currentPath *reactive.Signal[string] currentView *reactive.Signal[ui.View] notFound func() ui.View } func NewRouter(routes []Route) *Router { currentPath := reactive.NewSignal("") currentView := reactive.NewSignal(ui.NewView()) router := &Router{ routes: make([]Route, len(routes)), currentPath: ¤tPath, currentView: ¤tView, notFound: defaultNotFound, } // Parse route patterns for i, route := range routes { parsedRoute := route parsedRoute.parts, parsedRoute.params = parseRoute(route.Path) router.routes[i] = parsedRoute } return router } func (r *Router) SetNotFoundHandler(handler func() ui.View) { r.notFound = handler } func (r *Router) Navigate(path string) { r.currentPath.Set(path) r.updateView(path) } func (r *Router) GetCurrentPath() string { return r.currentPath.Get() } func (r *Router) GetCurrentView() ui.View { return r.currentView.Get() } func (r *Router) updateView(path string) { for _, route := range r.routes { if params := r.matchRoute(&route, path); params != nil { // Check guards if r.checkGuards(&route, params) { view := route.Handler(params) r.currentView.Set(view) return } } } // No route matched or guards failed r.currentView.Set(r.notFound()) } func (r *Router) matchRoute(route *Route, path string) map[string]string { // Strip query parameters for route matching if queryIndex := strings.Index(path, "?"); queryIndex != -1 { path = path[:queryIndex] } pathParts := strings.Split(strings.Trim(path, "/"), "/") if len(pathParts) == 1 && pathParts[0] == "" { pathParts = []string{} // Handle root path } if len(pathParts) != len(route.parts) { return nil } params := make(map[string]string) paramIndex := 0 for i, routePart := range route.parts { if strings.HasPrefix(routePart, ":") { // This is a parameter if paramIndex < len(route.params) { params[route.params[paramIndex]] = pathParts[i] paramIndex++ } } else { // Static part must match exactly if routePart != pathParts[i] { return nil } } } return params } func (r *Router) checkGuards(route *Route, params map[string]string) bool { for _, guard := range route.Guards { if !guard(route, params) { return false } } return true } func parseRoute(path string) ([]string, []string) { var params []string parts := strings.Split(strings.Trim(path, "/"), "/") if len(parts) == 1 && parts[0] == "" { parts = []string{} // Handle root path } // Find parameters like :id, :name, etc. for _, part := range parts { if strings.HasPrefix(part, ":") { paramName := part[1:] // Remove the ':' // Simple validation: param name should start with letter if len(paramName) > 0 && isLetter(paramName[0]) { params = append(params, paramName) } } } return parts, params } // isLetter checks if a byte is a letter func isLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } func defaultNotFound() ui.View { return ui.VerticalGroup( ui.TextFromString("404 - Page Not Found").Color("#ff4444"), ui.TextFromString("The requested page could not be found.").Color("#666"), ) }