This repository has no description
lustre gleam
0

Configure Feed

Select the types of activity you want to include in your feed.

sigo / client / src / client.gleam
9.1 kB 315 lines
1import client/page 2import client/page/home 3import client/page/landing 4import client/page/login 5import client/page/not_found 6import client/page/profile 7import client/page/register_crew 8import client/page/signup 9import client/ui/header 10import client/ui/sidebar 11import glaze/oat/sidebar as oat_sidebar 12import gleam/bool 13import gleam/http/response 14import gleam/option 15import lustre 16import lustre/effect.{type Effect} 17import lustre/element 18import lustre/element/html 19import modem 20import rsvp 21import shared/route 22import shared/session 23 24pub fn main() -> Nil { 25 let app = lustre.application(init:, update:, view:) 26 let assert Ok(_runtime) = lustre.start(app, "#app", Nil) 27 28 Nil 29} 30 31// MODEL ----------------------------------------------------------------------- 32 33pub type Model { 34 Model( 35 /// Current user 36 session: session.Session, 37 /// Current route 38 route: route.Route, 39 /// Current Page model 40 page: page.Page, 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 ProfileMsg(profile.Msg) 56 DashboardMsg(home.Msg) 57 LandingMsg(landing.Msg) 58 NotFoundMsg(not_found.Msg) 59 RegisterCrewMsg(register_crew.Msg) 60 61 // Session 62 UserRestoredSession(Result(session.Session, rsvp.Error)) 63 ServerRemovedToken(Result(response.Response(String), rsvp.Error)) 64} 65 66fn init(_props: Nil) -> #(Model, Effect(Msg)) { 67 let assert Ok(uri) = modem.initial_uri() 68 let route = route.parse(uri) 69 let page = page.init(route) 70 let is_protected = route.is_protected(route) 71 let session = init_session(route, is_protected) 72 73 let router_effect = 74 modem.init(fn(uri) { route.parse(uri) |> UserNavigatedTo }) 75 76 let session_effect = 77 UserRestoredSession 78 |> rsvp.expect_json(session.decoder(), _) 79 |> rsvp.get("/api/whoami", _) 80 81 let effect = effect.batch([session_effect, router_effect]) 82 #(Model(session:, route:, page:), effect) 83} 84 85fn init_session(route: route.Route, is_protected: Bool) -> session.Session { 86 case route { 87 route.Login | route.Landing -> 88 session.Pending(on_success: route.Home, on_error: route) 89 90 route if is_protected -> 91 session.Pending(on_success: route, on_error: route.Login) 92 93 _ -> session.None 94 } 95} 96 97fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 98 case model, msg { 99 // NAVIGATION -------------------------------------------------------------- 100 // 101 model, UserNavigatedTo(route) -> handle_navigation(model, route) 102 103 // SESSION MANAGEMENT ------------------------------------------------------- 104 // 105 Model(session: session.Pending(on_success:, on_error: _), ..), 106 UserRestoredSession(Ok(session)) 107 -> #( 108 Model(session:, route: on_success, page: page.init(on_success)), 109 modem.push(route.to_path(on_success), option.None, option.None), 110 ) 111 112 Model(session: session.Pending(on_success: _, on_error:), ..), 113 UserRestoredSession(Error(_)) 114 -> #( 115 Model(..model, route: on_error, page: page.init(on_error)), 116 modem.push(route.to_path(on_error), option.None, option.None), 117 ) 118 119 model, ServerRemovedToken(Ok(_)) -> { 120 let session = session.None 121 let redirect_to = route.to_path(route.Landing) 122 123 let new = Model(..model, session:) 124 let effect = modem.push(redirect_to, option.None, option.None) 125 126 #(new, effect) 127 } 128 129 // UI ---------------------------------------------------------------------- 130 // 131 // HEADER 132 model, HeaderMsg(_msg) -> #(model, effect.none()) 133 134 // SIDEBAR 135 model, SidebarMsg(msg) -> handle_sidebar_msg(model, msg) 136 137 // PAGES ------------------------------------------------------------------- 138 // 139 // LOGIN 140 Model(route: route.Login, page: page.Login(page_model), ..), 141 LoginMsg(page_msg) 142 -> handle_login_msg(model, page_model, page_msg) 143 144 // SIGNUP 145 Model(route: route.Signup, page: page.Signup(page_model), ..), 146 SignupMsg(page_msg) 147 -> 148 handle_msg( 149 model:, 150 page_model:, 151 page_model_fn: page.Signup, 152 page_msg:, 153 page_msg_map: SignupMsg, 154 update_fn: signup.update, 155 ) 156 157 // REGISTER CREW 158 Model(route: route.RegisterCrew, page: page.RegisterCrew(page_model), ..), 159 RegisterCrewMsg(page_msg) 160 -> 161 handle_msg( 162 model:, 163 page_model:, 164 page_model_fn: page.RegisterCrew, 165 page_msg:, 166 page_msg_map: RegisterCrewMsg, 167 update_fn: register_crew.update, 168 ) 169 170 // PROFILE 171 Model(route: route.Profile, page: page.Profile(_page_model), ..), 172 ProfileMsg(_page_msg) 173 -> #(model, effect.none()) 174 175 // LANDING 176 Model(route: route.Landing, page: page.Landing(_), ..), LandingMsg(_) -> #( 177 model, 178 effect.none(), 179 ) 180 181 // FALLBACK ---------------------------------------------------------------- 182 // 183 _, _ -> #(model, effect.none()) 184 } 185} 186 187// VIEW ------------------------------------------------------------------------ 188 189fn layout( 190 model: Model, 191 element_view: element.Element(a), 192 f: fn(a) -> Msg, 193) -> element.Element(Msg) { 194 oat_sidebar.sidebar(html.main, [], [ 195 header.view(model.session) |> element.map(HeaderMsg), 196 sidebar.view(model.session) |> element.map(SidebarMsg), 197 element_view |> element.map(f), 198 ]) 199} 200 201fn view(model: Model) -> element.Element(Msg) { 202 case model { 203 // LANDING PAGE ------------------------------------------------------------ 204 Model(session:, route: route.Landing, page: page.Landing(page_model)) -> 205 layout(model, landing.view(session, page_model), LandingMsg) 206 207 // LOGIN PAGE -------------------------------------------------------------- 208 Model(session:, route: route.Login, page: page.Login(page_model)) -> 209 layout(model, login.view(session, page_model), LoginMsg) 210 211 // DASHBOARD PAGE ---------------------------------------------------------- 212 Model(session:, route: route.Home, page: page.Dashboard(page_model)) -> 213 layout(model, home.view(session, page_model), DashboardMsg) 214 215 // SIGNUP PAGE ------------------------------------------------------------- 216 Model(session:, route: route.Signup, page: page.Signup(page_model)) -> 217 layout(model, signup.view(session, page_model), SignupMsg) 218 219 Model(session:, route: route.Profile, page: page.Profile(page_model)) -> 220 layout(model, profile.view(session, page_model), ProfileMsg) 221 222 // REGISTER CREW 223 Model( 224 session:, 225 route: route.RegisterCrew, 226 page: page.RegisterCrew(page_model), 227 ) -> layout(model, register_crew.view(session, page_model), RegisterCrewMsg) 228 229 // FALLBACK ---------------------------------------------------------------- 230 model -> layout(model, not_found.view(), NotFoundMsg) 231 } 232} 233 234// MESSAGE HANDLERS ------------------------------------------------------------ 235 236fn handle_navigation(model: Model, route: route.Route) -> #(Model, Effect(Msg)) { 237 use <- bool.guard(model.route == route, #(model, effect.none())) 238 let is_protected = route.is_protected(route) 239 240 let route = case model.session, route { 241 session.None, _ | session.Pending(_, _), _ if is_protected -> route.Login 242 session.Authenticated(_), route.Login -> route.Home 243 244 _, _ -> route 245 } 246 247 let new = Model(..model, route:, page: page.init(route)) 248 #(new, effect.none()) 249} 250 251/// Default message handling. 252/// Updates the current page model. 253fn handle_msg( 254 model model: Model, 255 page_model page_model: a, 256 page_model_fn page_model_fn: fn(a) -> page.Page, 257 page_msg page_msg: b, 258 page_msg_map page_msg_map: fn(b) -> Msg, 259 update_fn update_fn: fn(a, b) -> #(a, Effect(b)), 260) -> #(Model, Effect(Msg)) { 261 let #(page_model, effect) = update_fn(page_model, page_msg) 262 let page = page_model_fn(page_model) 263 264 let new = Model(..model, page:) 265 let effect = effect.map(effect, page_msg_map) 266 267 #(new, effect) 268} 269 270fn handle_sidebar_msg(model: Model, msg: sidebar.Msg) -> #(Model, Effect(Msg)) { 271 case model, msg { 272 Model(session: session.Authenticated(_), ..), sidebar.UserClickedLogout -> #( 273 model, 274 ServerRemovedToken 275 |> rsvp.expect_ok_response 276 |> rsvp.get("/api/logout", _), 277 ) 278 279 _, _ -> #(model, effect.none()) 280 } 281} 282 283fn handle_login_msg( 284 model: Model, 285 page_model: login.Model, 286 msg: login.Msg, 287) -> #(Model, Effect(Msg)) { 288 case login.update(page_model, msg) { 289 login.Continue(page_model, effect) -> #( 290 Model(..model, page: page.Login(page_model)), 291 effect.map(effect, LoginMsg), 292 ) 293 294 login.ServerAuthenticatedUser(session) -> #( 295 Model(..model, session:), 296 route.Home 297 |> route.to_path 298 |> modem.push(option.None, option.None), 299 ) 300 301 login.ServerFailedToAuthenticate(reason) -> { 302 let message = case reason { 303 rsvp.HttpError(resp) -> resp.body 304 rsvp.NetworkError -> "Connection unnavailable" 305 306 _ -> "" 307 } 308 309 let page_model = login.Model(..page_model, loading: False, message:) 310 let new = Model(..model, page: page.Login(page_model)) 311 312 #(new, effect.none()) 313 } 314 } 315}