this repo has no description
lustre frontent oat-ui gleam
1
fork

Configure Feed

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

at main 280 lines 8.5 kB view raw
1import client/language as lang 2import client/page 3import client/page/dashboard 4import client/page/home 5import client/page/login 6import client/page/navbar 7import client/page/not_found 8import client/page/sidebar 9import client/route 10import client/session 11import gleam/bool 12import gleam/http/response 13import gleam/option 14import gleam/uri 15import lustre 16import lustre/attribute as attr 17import lustre/effect.{type Effect} 18import lustre/element 19import lustre/element/html 20import modem 21import rsvp 22 23pub type Model { 24 Model( 25 /// Current user 26 session: session.Session, 27 /// Current route 28 route: route.Route, 29 /// Current Page model 30 page: page.Page, 31 /// Selected language 32 lang: lang.Language, 33 ) 34} 35 36pub fn main() -> Nil { 37 let assert Ok(uri) = modem.initial_uri() 38 39 let app = lustre.application(init:, update:, view:) 40 let assert Ok(_runtime) = lustre.start(app, "#app", InitOpts(uri)) 41 42 Nil 43} 44 45/// Data passed to our application during startup 46pub type InitOpts { 47 InitOpts( 48 /// Uri of the page when it first loaded. 49 uri: uri.Uri, 50 ) 51} 52 53pub type Msg { 54 /// Handle internal links navigation 55 UserNavigatedTo(to: route.Route) 56 /// User sent a request to the Server during startup to verify if the current 57 /// token is still valid. 58 UserRestoredSession(result: Result(session.Session, rsvp.Error)) 59 /// Server removed the token from the Client, ending their session. 60 ServerRemovedToken(result: Result(response.Response(String), rsvp.Error)) 61 62 // PAGES --- 63 LoginMsg(msg: login.Msg) 64 HomeMsg(msg: home.Msg) 65 DashboardMsg(msg: dashboard.Msg) 66 67 // UI Elements 68 SidebarMsg(msg: sidebar.Msg) 69 NavbarMsg(msg: navbar.Msg) 70 NotFoundMsg(msg: not_found.Msg) 71} 72 73// INIT ------------------------------------------------------------------------ 74 75/// Build initial application Model 76pub fn init(opts: InitOpts) -> #(Model, Effect(Msg)) { 77 let route = route.parse(opts.uri) 78 let page = page.init(route) 79 let is_protected = route.is_protected(route) 80 let session = init_session(route:, is_protected:) 81 82 let init_modem = { 83 use uri <- modem.init 84 UserNavigatedTo(route.parse(uri)) 85 } 86 87 // 󱡯 Send a request to the Server to verify if the current 88 // token is still valid. 89 // 90 // Allow access to the route if it succeeds and 91 // redirect User to the Login page in case of failure. 92 let restore_session = 93 UserRestoredSession 94 |> rsvp.expect_json(session.decoder(), _) 95 |> rsvp.get("/api/whoami", _) 96 97 // Batch all scheduled effects and build the initial application `Model` 98 let effect = effect.batch([restore_session, init_modem]) 99 #(Model(session:, route:, page:, lang: lang.BrazillianPortuguese), effect) 100} 101 102fn init_session( 103 route route: route.Route, 104 is_protected is_protected: Bool, 105) -> session.Session { 106 case route { 107 route.Login | route.Home -> 108 session.Pending(on_success: route.Home, on_error: route) 109 110 route if is_protected -> 111 session.Pending(on_success: route, on_error: route.Login) 112 113 _ -> session.Guest 114 } 115} 116 117// VIEW ------------------------------------------------------------------------ 118 119fn layout( 120 model: Model, 121 element_view: element.Element(a), 122 f: fn(a) -> Msg, 123) -> element.Element(Msg) { 124 html.section([attr.data("sidebar-layout", "")], [ 125 navbar.view(model.session, model.lang) |> element.map(NavbarMsg), 126 sidebar.view(model.session) |> element.map(SidebarMsg), 127 html.main([attr.class("p-0")], [element_view |> element.map(f)]), 128 ]) 129} 130 131/// Render page HTML 132pub fn view(model: Model) -> element.Element(Msg) { 133 case model { 134 // HOME PAGE --------------------------------------------------------------- 135 Model(route: route.Home, page: page.Home, ..) -> 136 layout(model, home.view(model.session), HomeMsg) 137 138 // LOGIN PAGE -------------------------------------------------------------- 139 Model(route: route.Login, page: page.Login(page), ..) -> 140 layout(model, login.view(page, model.lang), LoginMsg) 141 142 // DASHBOARD PAGE ---------------------------------------------------------- 143 Model(route: route.Dashboard, page: page.Dashboard(page), ..) -> 144 layout(model, dashboard.view(model.session, page), DashboardMsg) 145 146 _ -> layout(model, not_found.view(), NotFoundMsg) 147 } 148} 149 150// UPDATE ---------------------------------------------------------------------- 151 152/// Update current application `Model` 153pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 154 case model, msg { 155 //  NAVIGATION 156 model, UserNavigatedTo(route) -> handle_navigation(model, route) 157 158 //  LANGUAGE SELECTION 159 model, NavbarMsg(navbar.UserSelectedLanguage(lang)) -> #( 160 Model(..model, lang:), 161 effect.none(), 162 ) 163 164 // SESSION MANAGEMENT ----------------------------------------------------- 165 // 166 // If the Server successfully authenticated the User, 167 // initialize its Session, and redirect them to the correct route. 168 Model(session: session.Pending(on_success:, on_error: _), ..), 169 UserRestoredSession(Ok(session)) 170 -> #( 171 Model(..model, session:, route: on_success, page: page.init(on_success)), 172 modem.push(route.to_path(on_success), option.None, option.None), 173 ) 174 175 // If it fails, start the Session as a Guest and redirect 176 // the User accordingly, usually to the Login Page. 177 Model(session: session.Pending(on_success: _, on_error:), ..), 178 UserRestoredSession(Error(_)) 179 -> #( 180 Model( 181 ..model, 182 route: on_error, 183 page: page.init(on_error), 184 session: session.Guest, 185 ), 186 modem.push(route.to_path(on_error), option.None, option.None), 187 ) 188 189 // User ended their Session and token has been removed. 190 // Redirect user to the Home page. 191 model, ServerRemovedToken(Ok(_)) -> { 192 let session = session.Guest 193 let route = route.to_path(route.Home) 194 195 #(Model(..model, session:), modem.push(route, option.None, option.None)) 196 } 197 198 // PAGES ------------------------------------------------------------------- 199 // 200 // LOGIN 201 Model(route: route.Login, page: page.Login(page), ..), LoginMsg(page_msg) -> 202 handle_login_step(model, page, page_msg) 203 204 // FALLBACK 205 // 206 _, _ -> #(model, effect.none()) 207 } 208} 209 210fn handle_navigation( 211 model: Model, 212 route: route.Route, 213) -> #(Model, Effect(Msg)) { 214 // Do nothing the route doesnt change 215 use <- bool.guard(model.route == route, #(model, effect.none())) 216 let protected = route.is_protected(route) 217 218 let route = case model.session, route { 219 // If the route require the User to be authenticated, 220 // redirect them to the Login page. 221 session.Guest, _ | session.Pending(_, _), _ if protected -> route.Login 222 // If the User is *already* authenticated but navigating to 223 // the Login page, redirect them to Dashboard instead. 224 session.Authenticated(_), route.Login -> route.Dashboard 225 226 _, _ -> route 227 } 228 229 let page = page.init(route) 230 #(Model(..model, route:, page:), effect.none()) 231} 232 233fn handle_login_step( 234 model: Model, 235 page: login.Model, 236 msg: login.Msg, 237) -> #(Model, Effect(Msg)) { 238 case login.update(page, msg) { 239 // Clear any remaining error message and continue execution as normal. 240 login.Continue(page, effect) -> #( 241 Model(..model, page: page.Login(page)), 242 effect.map(effect, LoginMsg), 243 ) 244 245 //  Server successfully authenticated the Client, we can now store 246 // the returned Session token in our application Model and access 247 // protected routes. 248 login.ServerAuthenticatedUser(session) -> #( 249 Model(..model, session:), 250 route.Dashboard 251 |> route.to_path 252 |> modem.push(option.None, option.None), 253 ) 254 255 //  Server failed to authenticate the Client, display a error message 256 // on the Login page and continue execution. 257 login.ServerFailedToAuthenticate(reason) -> { 258 let message = case reason { 259 rsvp.HttpError(resp) -> resp.body 260 261 rsvp.NetworkError -> 262 case model.lang { 263 lang.BrazillianPortuguese | lang.Portuguese -> 264 "Conexão não disponível" 265 _ -> "Connection not available" 266 } 267 268 _ -> 269 case model.lang { 270 lang.BrazillianPortuguese | lang.Portuguese -> 271 "Ocorreu um erro ao enviar credenciais" 272 _ -> "An error occurred when sending credentials" 273 } 274 } 275 276 let page = login.Model(..page, loading: False, message:) 277 #(Model(..model, page: page.Login(page)), effect.none()) 278 } 279 } 280}