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

Configure Feed

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

:construction: add login model, login msg, and basic message handling

+245 -10
+1
client/gleam.toml
··· 8 8 modem = ">= 2.1.2 and < 3.0.0" 9 9 shared = { path = "../shared" } 10 10 rsvp = ">= 1.2.0 and < 2.0.0" 11 + gleam_http = ">= 4.3.0 and < 5.0.0" 11 12 12 13 [dev_dependencies] 13 14 gleeunit = ">= 1.0.0 and < 2.0.0"
+1
client/manifest.toml
··· 46 46 ] 47 47 48 48 [requirements] 49 + gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 49 50 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 50 51 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 51 52 lustre = { version = ">= 5.6.0 and < 6.0.0" }
+132 -1
client/src/client.gleam
··· 1 1 import client/page 2 + import client/page/home 3 + import client/page/login 4 + import client/page/not_found 5 + import gleam/bool 6 + import gleam/http/response 7 + import gleam/option 2 8 import lustre 3 9 import lustre/effect.{type Effect} 10 + import lustre/element 4 11 import modem 5 12 import rsvp 6 13 import shared/route ··· 18 25 } 19 26 20 27 pub fn main() -> Nil { 21 - let app = lustre.application(init:, update: todo, view: todo) 28 + let app = lustre.application(init:, update:, view:) 22 29 let assert Ok(_runtime) = lustre.start(app, "#app", Nil) 23 30 24 31 Nil ··· 27 34 pub type Msg { 28 35 UserNavigatedTo(route.Route) 29 36 UserRestoredSession(Result(session.Session, rsvp.Error)) 37 + 38 + LoginMsg(login.Msg) 39 + HomeMsg(home.Msg) 40 + 41 + ServerRemovedToken(Result(response.Response(String), rsvp.Error)) 30 42 } 43 + 44 + // INIT 31 45 32 46 pub fn init(_props: Nil) -> #(Model, Effect(Msg)) { 33 47 let assert Ok(uri) = modem.initial_uri() ··· 59 73 _ -> session.None 60 74 } 61 75 } 76 + 77 + fn layout( 78 + _model: Model, 79 + element_view: element.Element(a), 80 + f: fn(a) -> Msg, 81 + ) -> element.Element(Msg) { 82 + element_view |> element.map(f) 83 + } 84 + 85 + pub fn view(model: Model) -> element.Element(Msg) { 86 + case model { 87 + // HOME PAGE --------------------------------------------------------------- 88 + Model(session:, route: route.Home, page: page.Home) -> 89 + layout(model, home.view(session), HomeMsg) 90 + 91 + // LOGIN PAGE -------------------------------------------------------------- 92 + Model(session:, route: route.Login, page: page.Login(page_model)) -> 93 + layout(model, login.view(session, page_model), LoginMsg) 94 + 95 + _ -> not_found.view() 96 + } 97 + } 98 + 99 + // UPDATE 100 + pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 101 + case model, msg { 102 + // NAVIGATION -------------------------------------------------------------- 103 + // 104 + model, UserNavigatedTo(route) -> handle_navigation(model, route) 105 + 106 + // SESSION MANAGEMENT ------------------------------------------------------- 107 + // 108 + Model(session: session.Pending(on_success:, on_error: _), ..), 109 + UserRestoredSession(Ok(session)) 110 + -> #( 111 + Model(session:, route: on_success, page: page.init(on_success)), 112 + modem.push(route.to_path(on_success), option.None, option.None), 113 + ) 114 + 115 + Model(session: session.Pending(on_success: _, on_error:), ..), 116 + UserRestoredSession(Error(_)) 117 + -> #( 118 + Model(..model, route: on_error, page: page.init(on_error)), 119 + modem.push(route.to_path(on_error), option.None, option.None), 120 + ) 121 + 122 + model, ServerRemovedToken(Ok(_)) -> { 123 + let session = session.None 124 + let redirect_to = route.to_path(route.Home) 125 + 126 + let new = Model(..model, session:) 127 + let effect = modem.push(redirect_to, option.None, option.None) 128 + 129 + #(new, effect) 130 + } 131 + 132 + // PAGES ------------------------------------------------------------------- 133 + // 134 + // LOGIN 135 + Model(route: route.Login, page: page.Login(page_model), ..), 136 + LoginMsg(page_msg) 137 + -> handle_login_step(model, page_model, page_msg) 138 + 139 + // FALLBACK ---------------------------------------------------------------- 140 + // 141 + _, _ -> #(model, effect.none()) 142 + } 143 + } 144 + 145 + fn handle_navigation(model: Model, route: route.Route) -> #(Model, Effect(Msg)) { 146 + use <- bool.guard(model.route == route, #(model, effect.none())) 147 + let is_protected = route.is_protected(route) 148 + 149 + let route = case model.session, route { 150 + session.None, _ | session.Pending(_, _), _ if is_protected -> route.Login 151 + session.Authenticated(_), route.Login -> route.Home 152 + 153 + _, _ -> route 154 + } 155 + 156 + let new = Model(..model, route:, page: page.init(route)) 157 + #(new, effect.none()) 158 + } 159 + 160 + fn handle_login_step( 161 + model: Model, 162 + page_model: login.Model, 163 + msg: login.Msg, 164 + ) -> #(Model, Effect(Msg)) { 165 + case login.update(page_model, msg) { 166 + login.Continue(page_model, effect) -> #( 167 + Model(..model, page: page.Login(page_model)), 168 + effect.map(effect, LoginMsg), 169 + ) 170 + 171 + login.ServerAuthenticatedUser(session) -> #( 172 + Model(..model, session:), 173 + route.Home 174 + |> route.to_path 175 + |> modem.push(option.None, option.None), 176 + ) 177 + 178 + login.ServerFailedToAuthenticate(reason) -> { 179 + let message = case reason { 180 + rsvp.HttpError(resp) -> resp.body 181 + rsvp.NetworkError -> "Connection unnavailable" 182 + 183 + _ -> "" 184 + } 185 + 186 + let page_model = login.Model(..page_model, loading: False, message:) 187 + let new = Model(..model, page: page.Login(page_model)) 188 + 189 + #(new, effect.none()) 190 + } 191 + } 192 + }
+19
client/src/client/page/home.gleam
··· 1 + import lustre/effect 2 + import lustre/element 3 + import shared/session 4 + 5 + pub type Model { 6 + Model 7 + } 8 + 9 + pub type Msg 10 + 11 + pub const empty: Model = Model 12 + 13 + pub fn update(model: Model, _msg: Msg) -> #(Model, effect.Effect(Msg)) { 14 + #(model, effect.none()) 15 + } 16 + 17 + pub fn view(_session: session.Session) -> element.Element(Msg) { 18 + element.none() 19 + }
+52 -2
client/src/client/page/login.gleam
··· 1 + import lustre/effect 2 + import lustre/element 3 + import rsvp 4 + import shared/session 5 + 1 6 pub type Model { 2 - Model(email: String, password: String) 7 + Model(email: String, password: String, loading: Bool, message: String) 8 + } 9 + 10 + pub const empty = Model(email: "", password: "", loading: False, message: "") 11 + 12 + pub type Msg { 13 + /// User updated email field 14 + UserTypedEmail(email: String) 15 + /// User updated password field 16 + UserTypedPassword(password: String) 17 + 18 + /// User sent credentials to the Server 19 + UserClickedSubmit 20 + 21 + /// API validated User's credentials 22 + ApiReturnedSession(Result(session.Session, rsvp.Error)) 3 23 } 4 24 5 - pub const empty = Model(email: "", password: "") 25 + pub type LoginStep { 26 + Continue(model: Model, effect: effect.Effect(Msg)) 27 + ServerAuthenticatedUser(session: session.Session) 28 + ServerFailedToAuthenticate(reason: rsvp.Error) 29 + } 30 + 31 + pub fn update(model: Model, msg: Msg) -> LoginStep { 32 + case msg { 33 + UserTypedEmail(email:) -> { 34 + let model = Model(..model, email:) 35 + Continue(model, effect.none()) 36 + } 37 + 38 + UserTypedPassword(password:) -> { 39 + let model = Model(..model, password:) 40 + Continue(model, effect.none()) 41 + } 42 + 43 + UserClickedSubmit -> { 44 + let model = Model(..empty, loading: True) 45 + Continue(model, todo as "send request") 46 + } 47 + 48 + ApiReturnedSession(Ok(session)) -> ServerAuthenticatedUser(session) 49 + ApiReturnedSession(Error(reason)) -> ServerFailedToAuthenticate(reason) 50 + } 51 + } 52 + 53 + pub fn view(_session: session.Session, _model: Model) -> element.Element(Msg) { 54 + element.none() 55 + }
+18
client/src/client/page/not_found.gleam
··· 1 + import lustre/effect 2 + import lustre/element 3 + 4 + pub type Model { 5 + Model 6 + } 7 + 8 + pub type Msg 9 + 10 + pub const empty = Model 11 + 12 + pub fn update(model: Model, _msg: Msg) -> #(Model, effect.Effect(Msg)) { 13 + #(model, effect.none()) 14 + } 15 + 16 + pub fn view() { 17 + element.none() 18 + }
-5
shared/src/shared.gleam
··· 1 - import gleam/io 2 - 3 - pub fn main() -> Nil { 4 - io.println("Hello from shared!") 5 - }
+20
shared/src/shared/route.gleam
··· 1 + import gleam/dynamic/decode 1 2 import gleam/uri 2 3 3 4 pub type Route { ··· 7 8 NotFound 8 9 } 9 10 11 + pub fn decoder() -> decode.Decoder(Route) { 12 + use variant <- decode.then(decode.string) 13 + case variant { 14 + "home" -> decode.success(Home) 15 + "login" -> decode.success(Login) 16 + "not_found" -> decode.success(NotFound) 17 + 18 + _ -> decode.failure(NotFound, "Route") 19 + } 20 + } 21 + 10 22 pub fn is_protected(route: Route) -> Bool { 11 23 case route { 12 24 Login | NotFound -> False ··· 22 34 ["not-found"] | _ -> NotFound 23 35 } 24 36 } 37 + 38 + pub fn to_path(route: Route) -> String { 39 + case route { 40 + Home -> "/" 41 + Login -> "/login" 42 + NotFound -> "/not-found" 43 + } 44 + }
+2 -2
shared/src/shared/session.gleam
··· 18 18 } 19 19 20 20 "pending" -> { 21 - use on_success <- decode.field("on_success", todo) 22 - use on_error <- decode.field("on_error", todo) 21 + use on_success <- decode.field("on_success", route.decoder()) 22 + use on_error <- decode.field("on_error", route.decoder()) 23 23 decode.success(Pending(on_success:, on_error:)) 24 24 } 25 25