๐Ÿ‘ฉโ€๐Ÿš’ Firefighters API written in Gleam!
lustre gleam
at main 303 lines 8.7 kB view raw
1import client/page 2import client/page/dashboard 3import client/page/landing 4import client/page/login 5import client/page/not_found 6import client/page/signup 7import client/ui/header 8import client/ui/sidebar 9import gleam/bool 10import gleam/http/response 11import gleam/option 12import lustre 13import lustre/attribute.{class} 14import lustre/effect 15import lustre/element 16import lustre/element/html 17import modem 18import rsvp 19import shared/route 20import shared/session 21 22pub fn main() -> Nil { 23 let app = lustre.application(init:, update:, view:) 24 let assert Ok(_runtime) = lustre.start(app, "#app", Nil) 25 26 Nil 27} 28 29// MODEL ----------------------------------------------------------------------- 30 31pub type Model { 32 Model( 33 /// Current user 34 session: session.Session, 35 /// Current route 36 route: route.Route, 37 /// Current Page model 38 page: page.Page, 39 /// Siderbar model 40 sidebar: sidebar.Model, 41 ) 42} 43 44pub type Msg { 45 // Navigation 46 UserNavigatedTo(route.Route) 47 48 // Components 49 HeaderMsg(header.Msg) 50 SidebarMsg(sidebar.Msg) 51 52 // Pages 53 LoginMsg(login.Msg) 54 SignupMsg(signup.Msg) 55 DashboardMsg(dashboard.Msg) 56 LandingMsg(landing.Msg) 57 NotFoundMsg(not_found.Msg) 58 59 // Session 60 UserRestoredSession(Result(session.Session, rsvp.Error)) 61 ServerRemovedToken(Result(response.Response(String), rsvp.Error)) 62} 63 64fn init(_props: Nil) -> #(Model, effect.Effect(Msg)) { 65 let assert Ok(uri) = modem.initial_uri() 66 let route = route.parse(uri) 67 let page = page.init(route) 68 let is_protected = route.is_protected(route) 69 let session = init_session(route, is_protected) 70 71 let router_effect = 72 fn(uri) { UserNavigatedTo(route.parse(uri)) } 73 |> modem.init 74 75 let session_effect = 76 UserRestoredSession 77 |> rsvp.expect_json(session.decoder(), _) 78 |> rsvp.get("/api/whoami", _) 79 80 let effect = effect.batch([session_effect, router_effect]) 81 #(Model(session:, route:, page:, sidebar: sidebar.Closed), effect) 82} 83 84fn init_session(route: route.Route, is_protected: Bool) -> session.Session { 85 case route { 86 route.Login | route.Landing -> 87 session.Pending(on_success: route.Dashboard, on_error: route) 88 89 route if is_protected -> 90 session.Pending(on_success: route, on_error: route.Login) 91 92 _ -> session.None 93 } 94} 95 96fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { 97 case model, msg { 98 // UI ---------------------------------------------------------------------- 99 // 100 // HEADER 101 model, HeaderMsg(msg) -> handle_header_msg(model, msg) 102 103 // SIDEBAR 104 model, SidebarMsg(msg) -> handle_sidebar_msg(model, msg) 105 106 // PAGES ------------------------------------------------------------------- 107 // 108 // LOGIN 109 Model(route: route.Login, page: page.Login(page_model), ..), 110 LoginMsg(page_msg) 111 -> handle_login_msg(model, page_model, page_msg) 112 113 // SIGNUP 114 Model(route: route.Signup, page: page.Signup(page_model), ..), 115 SignupMsg(page_msg) 116 -> handle_signup_msg(model, page_model, page_msg) 117 118 // LANDING 119 Model(route: route.Landing, page: page.Landing(_), ..), LandingMsg(_) -> #( 120 model, 121 effect.none(), 122 ) 123 124 // NAVIGATION -------------------------------------------------------------- 125 // 126 model, UserNavigatedTo(route) -> handle_navigation(model, route) 127 128 // SESSION MANAGEMENT ------------------------------------------------------- 129 // 130 Model(session: session.Pending(on_success:, on_error: _), ..), 131 UserRestoredSession(Ok(session)) 132 -> #( 133 Model(..model, session:, route: on_success, page: page.init(on_success)), 134 modem.push(route.to_path(on_success), option.None, option.None), 135 ) 136 137 Model(session: session.Pending(on_success: _, on_error:), ..), 138 UserRestoredSession(Error(_)) 139 -> #( 140 Model(..model, route: on_error, page: page.init(on_error)), 141 modem.push(route.to_path(on_error), option.None, option.None), 142 ) 143 144 model, ServerRemovedToken(Ok(_)) -> { 145 let session = session.None 146 let model = Model(..model, session:) 147 let redirect_to = route.to_path(route.Landing) 148 149 #(model, modem.push(redirect_to, option.None, option.None)) 150 } 151 152 // FALLBACK ---------------------------------------------------------------- 153 // 154 model, _ -> #(model, effect.none()) 155 } 156} 157 158// VIEW ------------------------------------------------------------------------ 159 160fn view_page( 161 model: Model, 162 element_view: element.Element(a), 163 f: fn(a) -> Msg, 164) -> element.Element(Msg) { 165 let style = class("flex relative flex-col p-4 w-full min-h-dvh bg-background") 166 167 html.main([style], [ 168 header.view(model.session) |> element.map(HeaderMsg), 169 sidebar.view(model.session, model.sidebar) |> element.map(SidebarMsg), 170 element_view |> element.map(f), 171 ]) 172} 173 174fn view(model: Model) -> element.Element(Msg) { 175 case model { 176 // LANDING PAGE ------------------------------------------------------------ 177 Model(session:, route: route.Landing, page: page.Landing(page_model), ..) -> 178 view_page(model, landing.view(session, page_model), LandingMsg) 179 180 // LOGIN PAGE -------------------------------------------------------------- 181 Model(session:, route: route.Login, page: page.Login(page_model), ..) -> 182 view_page(model, login.view(session, page_model), LoginMsg) 183 184 // DASHBOARD PAGE ---------------------------------------------------------- 185 Model( 186 session:, 187 route: route.Dashboard, 188 page: page.Dashboard(page_model), 189 .., 190 ) -> view_page(model, dashboard.view(session, page_model), DashboardMsg) 191 192 // SIGNUP PAGE ------------------------------------------------------------- 193 Model(session:, route: route.Signup, page: page.Signup(page_model), ..) -> 194 view_page(model, signup.view(session, page_model), SignupMsg) 195 196 // FALLBACK ---------------------------------------------------------------- 197 model -> view_page(model, not_found.view(), NotFoundMsg) 198 } 199} 200 201// MESSAGE HANDLERS ------------------------------------------------------------ 202 203fn handle_navigation( 204 model: Model, 205 route: route.Route, 206) -> #(Model, effect.Effect(Msg)) { 207 use <- bool.guard(model.route == route, #(model, effect.none())) 208 let is_protected = route.is_protected(route) 209 210 let route = case model.session, route { 211 session.None, _ | session.Pending(_, _), _ if is_protected -> route.Login 212 session.Authenticated(_), route.Login -> route.Dashboard 213 _, route -> route 214 } 215 216 let model = Model(..model, route:, page: page.init(route)) 217 #(model, effect.none()) 218} 219 220fn handle_header_msg( 221 model: Model, 222 msg: header.Msg, 223) -> #(Model, effect.Effect(Msg)) { 224 case model, msg { 225 Model(session: session.Authenticated(_), ..), header.UserClickedLogout -> { 226 let message = ServerRemovedToken 227 let effect = rsvp.get("/api/logout", rsvp.expect_ok_response(message)) 228 229 #(model, effect) 230 } 231 232 Model(sidebar: sidebar.Closed, ..), header.UserToggledSidebar -> #( 233 Model(..model, sidebar: sidebar.Open), 234 effect.none(), 235 ) 236 237 Model(sidebar: sidebar.Open, ..), header.UserToggledSidebar -> #( 238 Model(..model, sidebar: sidebar.Closed), 239 effect.none(), 240 ) 241 242 _, _ -> #(model, effect.none()) 243 } 244} 245 246fn handle_sidebar_msg( 247 model: Model, 248 msg: sidebar.Msg, 249) -> #(Model, effect.Effect(Msg)) { 250 case model, msg { 251 Model(sidebar: sidebar.Open, ..), sidebar.UserClickedBackdrop -> #( 252 Model(..model, sidebar: sidebar.Closed), 253 effect.none(), 254 ) 255 256 _, _ -> #(model, effect.none()) 257 } 258} 259 260fn handle_login_msg( 261 model: Model, 262 page_model: login.Model, 263 msg: login.Msg, 264) -> #(Model, effect.Effect(Msg)) { 265 case login.update(page_model, msg) { 266 login.Continue(page_model, effect) -> #( 267 Model(..model, page: page.Login(page_model)), 268 effect.map(effect, LoginMsg), 269 ) 270 271 login.ServerAuthenticatedUser(session) -> #( 272 Model(..model, session:), 273 route.Dashboard 274 |> route.to_path 275 |> modem.push(option.None, option.None), 276 ) 277 278 login.ServerFailedToAuthenticate(err) -> { 279 let message = case err { 280 rsvp.HttpError(resp) -> resp.body 281 rsvp.NetworkError -> "Connection not available" 282 _ -> "Failed to authenticate user" 283 } 284 285 let page = 286 login.Model(..page_model, message:, loading: False) |> page.Login 287 288 #(Model(..model, page:), effect.none()) 289 } 290 } 291} 292 293fn handle_signup_msg( 294 model: Model, 295 page_model: signup.Model, 296 page_msg: signup.Msg, 297) -> #(Model, effect.Effect(Msg)) { 298 let #(page_model, effect) = signup.update(page_model, page_msg) 299 let page = page.Signup(page_model) 300 let effect = effect.map(effect, SignupMsg) 301 302 #(Model(..model, page:), effect) 303}