This repository has no description
lustre gleam
0

Configure Feed

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

:beer: back to using strings

author
kacaii.dev
date (Apr 6, 2026, 3:52 PM -0300) commit ca1970f2 parent a6a09be0 change-id vrxmmtrx
+218 -388
+51 -82
client/src/client/page/login.gleam
··· 1 1 import client/ui/header 2 - import client/ui/input 3 2 import glaze/oat/alert 4 3 import glaze/oat/card 5 4 import glaze/oat/form 5 + import gleam/result 6 6 import lustre/attribute.{class} as attr 7 7 import lustre/effect.{type Effect} 8 8 import lustre/element ··· 13 13 import shared/session 14 14 import shared/user/email 15 15 16 - pub const empty = Model( 17 - email: input.Input( 18 - value: "", 19 - placeholder: "user@email.dev", 20 - hint: "", 21 - label: "Email", 22 - input_type: "email", 23 - id: "input-email", 24 - is_valid: True, 25 - ), 26 - password: input.Input( 27 - value: "", 28 - placeholder: "********", 29 - hint: "", 30 - label: "Password", 31 - input_type: "password", 32 - id: "input-password", 33 - is_valid: True, 34 - ), 35 - loading: False, 36 - message: "", 37 - ) 16 + pub const empty = Model(email: "", password: "", loading: False, message: "") 38 17 39 18 pub type Model { 40 - Model( 41 - email: input.Input, 42 - password: input.Input, 43 - loading: Bool, 44 - message: String, 45 - ) 19 + Model(email: String, password: String, loading: Bool, message: String) 46 20 } 47 21 48 22 pub type Msg { ··· 68 42 69 43 pub fn update(model: Model, msg: Msg) -> LoginStep { 70 44 case msg { 71 - UserTypedEmail(value:) -> { 72 - let email = case email.parse(value) { 73 - _ if value == "" -> { 74 - let is_valid = False 75 - let hint = "Email cannot be empty" 76 - input.Input(..model.email, value:, hint:, is_valid:) 77 - } 45 + UserTypedEmail(value:) -> 46 + Continue(Model(..model, email: value), effect.none()) 78 47 79 - Ok(_) -> { 80 - let is_valid = True 81 - let hint = "" 82 - input.Input(..model.email, value:, hint:, is_valid:) 83 - } 84 - 85 - Error(_) -> { 86 - let is_valid = False 87 - let hint = "Provide a valid email address" 88 - input.Input(..model.email, value:, hint:, is_valid:) 89 - } 90 - } 91 - 92 - let new = Model(..model, email:) 93 - Continue(new, effect.none()) 94 - } 95 - 96 - UserTypedPassword(value:) -> { 97 - let password = case value { 98 - "" -> { 99 - let is_valid = False 100 - let hint = "Password cant be empty" 101 - input.Input(..model.password, value:, hint:, is_valid:) 102 - } 103 - 104 - _ -> { 105 - let is_valid = True 106 - let hint = "" 107 - input.Input(..model.password, value:, hint:, is_valid:) 108 - } 109 - } 110 - 111 - let new = Model(..model, password:) 112 - Continue(new, effect.none()) 113 - } 48 + UserTypedPassword(value:) -> 49 + Continue(Model(..model, password: value), effect.none()) 114 50 115 51 UserClickedSubmit -> { 116 - let email = model.email.value 117 - let password = model.password.value 118 - 119 52 let body = 120 - contract.RequestBody(email:, password:) 53 + contract.RequestBody(email: model.email, password: model.password) 121 54 |> contract.request_to_json 122 55 123 56 let handler = 124 57 ApiReturnedSession 125 58 |> rsvp.expect_json(contract.response_decoder(), _) 126 59 127 - let new = Model(..model, loading: True) 128 - Continue(new, rsvp.post(contract.url, body, handler)) 60 + Continue( 61 + Model(..model, loading: True), 62 + rsvp.post(contract.url, body, handler), 63 + ) 129 64 } 130 65 131 66 // auth ··· 149 84 card.header([], [html.h3([], [html.text("Login")])]), 150 85 card.content([], [ 151 86 view_fieldset([ 152 - input.view(field: model.email, on_input: UserTypedEmail), 153 - input.view(field: model.password, on_input: UserTypedPassword), 87 + view_input( 88 + value: model.email, 89 + label: "Email", 90 + placeholder: "user@email.com", 91 + input_type: "email", 92 + on_input: UserTypedEmail, 93 + ), 94 + 95 + view_input( 96 + value: model.password, 97 + label: "Password", 98 + placeholder: "********", 99 + input_type: "password", 100 + on_input: UserTypedPassword, 101 + ), 154 102 ]), 155 103 ]), 156 104 ··· 161 109 ]) 162 110 } 163 111 112 + pub fn view_input( 113 + value value: String, 114 + label label: String, 115 + placeholder placeholder: String, 116 + input_type input_type: String, 117 + on_input on_input: fn(String) -> Msg, 118 + ) -> element.Element(Msg) { 119 + let input_attributes = [ 120 + event.on_input(on_input), 121 + 122 + attr.placeholder(placeholder), 123 + attr.value(value), 124 + attr.type_(input_type), 125 + ] 126 + 127 + html.label([], [ 128 + html.text(label), 129 + html.input(input_attributes), 130 + ]) 131 + } 132 + 164 133 fn view_alert(model: Model) { 165 134 case model.message { 166 135 "" -> element.none() ··· 177 146 False -> class("hover:cursor-pointer") 178 147 } 179 148 180 - let valid_email = model.email.is_valid 181 - let valid_password = model.password.is_valid 149 + let disabled = 150 + email.parse(model.email) |> result.is_error() || model.password == "" 182 151 183 152 let attributes = [ 184 153 hover_style, 185 154 class("w-full"), 186 155 187 156 attr.aria_busy(model.loading), 188 - attr.disabled(!valid_email || !valid_password), 157 + attr.disabled(disabled), 189 158 event.on_click(UserClickedSubmit), 190 159 ] 191 160
+50 -46
client/src/client/page/register_crew.gleam
··· 1 1 import client/ui/header 2 - import client/ui/input 3 - import client/ui/user_card 4 2 import glaze/oat/button 5 3 import glaze/oat/card 6 4 import glaze/oat/form ··· 23 21 pub const empty: Model = Model( 24 22 selected_leader: option.None, 25 23 crew_leader_name: "", 26 - crew_name: input.Input( 27 - hint: "", 28 - id: "crew-name", 29 - input_type: "text", 30 - is_valid: True, 31 - label: "Crew name", 32 - placeholder: "Crew ABC", 33 - value: "", 34 - ), 24 + crew_name: "", 35 25 selected_members: [], 36 26 suggested_leaders: [], 37 27 suggested_members: [], ··· 39 29 40 30 pub type Model { 41 31 Model( 42 - crew_name: input.Input, 32 + crew_name: String, 43 33 crew_leader_name: String, 44 34 selected_leader: option.Option(User), 45 35 selected_members: List(User), ··· 87 77 ) 88 78 } 89 79 90 - UserTypedCrewName(value) -> { 91 - let crew_name = case value { 92 - "" -> { 93 - let hint = "Crew's name cant be empty" 94 - let is_valid = False 95 - input.Input(..model.crew_name, value:, hint:, is_valid:) 96 - } 97 - 98 - value -> { 99 - let hint = "" 100 - let is_valid = True 101 - input.Input(..model.crew_name, value:, hint:, is_valid:) 102 - } 103 - } 104 - 105 - #(Model(..model, crew_name:), effect.none()) 106 - } 80 + UserTypedCrewName(value) -> #( 81 + Model(..model, crew_name: value), 82 + effect.none(), 83 + ) 107 84 108 85 UserTypedLeaderName(value) -> { 109 86 let new = case value { ··· 159 136 160 137 html.section(attributes, [ 161 138 view_form([ 162 - input.view(model.crew_name, UserTypedCrewName), 139 + view_crew_name_input(model), 163 140 view_leader_selection(model), 164 141 view_submit_button(), 165 142 ]), 166 143 ]) 167 144 } 168 145 146 + fn view_crew_name_input(model: Model) -> element.Element(Msg) { 147 + let id = "input-crew-name" 148 + 149 + html.div([], [ 150 + html.label([attr.for(id)], [html.text("Crew name")]), 151 + html.input([ 152 + attr.id(id), 153 + event.on_input(UserTypedCrewName), 154 + attr.placeholder("Crew ABC"), 155 + attr.value(model.crew_name), 156 + attr.type_("text"), 157 + ]), 158 + ]) 159 + } 160 + 169 161 fn view_form(content: List(element.Element(Msg))) -> element.Element(Msg) { 170 162 let attributes = [class("flex flex-col gap-4 w-full")] 171 163 ··· 176 168 } 177 169 178 170 fn view_leader_selection(model: Model) -> element.Element(Msg) { 179 - let input_id = "leader-name-input" 180 - 181 - let attributes = [ 182 - form.field(), 183 - attr.type_("text"), 184 - attr.id(input_id), 185 - attr.value(model.crew_leader_name), 186 - attr.placeholder("John Doe"), 187 - 188 - event.on_input(UserTypedLeaderName), 189 - ] 171 + let id = "leader-name-input" 190 172 191 173 let search_button = { 192 174 let on_click = event.on_click(UserClickedSearchLeader) ··· 199 181 button.outline(), 200 182 attr.disabled(email_is_empty || email_is_too_short), 201 183 202 - class("h-full"), 184 + class("size-auto"), 203 185 class("not-disabled:hover:text-background"), 204 186 ] 205 187 206 - button.button(attributes, [icon.mail_search([])]) 188 + button.button(attributes, [icon.user_search([])]) 207 189 } 208 190 209 191 let view_leaders = case model.suggested_leaders { 210 192 [] -> element.none() 211 193 some -> 212 194 [class("grid grid-cols-1 gap-4"), class("@md:grid-cols-2")] 213 - |> html.div(list.map(some, user_card.view(_, UserSelectedLeader))) 195 + |> html.div(list.map(some, view_leader_card(_, UserSelectedLeader))) 214 196 } 215 197 216 198 html.div([class("@container")], [ 217 - html.label([attr.for(input_id), class("py-1")], [html.text("Leader")]), 218 - form.fieldset([form.group()], [form.input(attributes), search_button]), 199 + html.label([attr.for(id)], [html.text("Leader")]), 200 + form.fieldset([form.group(), class("mt-1")], [ 201 + html.input([ 202 + attr.id(id), 203 + event.on_input(UserTypedLeaderName), 204 + attr.placeholder("John Doe"), 205 + attr.value(model.crew_leader_name), 206 + attr.type_("text"), 207 + ]), 208 + search_button, 209 + ]), 219 210 view_leaders, 211 + ]) 212 + } 213 + 214 + pub fn view_leader_card( 215 + user: User, 216 + on_click: fn(User) -> Msg, 217 + ) -> element.Element(Msg) { 218 + let on_click = event.on_click(on_click(user)) 219 + let hover_style = class("hover:cursor-pointer hover:border-accent") 220 + 221 + card.card([on_click, hover_style, class("flex flex-col gap-1 p-3")], [ 222 + card.header([], [html.strong([], [html.text(user.full_name)])]), 223 + card.content([], [html.div([], [html.small([], [html.text(user.email)])])]), 220 224 ]) 221 225 } 222 226
+87 -191
client/src/client/page/signup.gleam
··· 1 1 import client/ui/header 2 - import client/ui/input 3 2 import glaze/oat/card 4 3 import glaze/oat/form 5 4 import gleam/list 5 + import gleam/string 6 6 import lustre/attribute.{class} as attr 7 7 import lustre/effect.{type Effect} 8 8 import lustre/element ··· 12 12 import shared/contract/signup as contract 13 13 import shared/role 14 14 import shared/session 15 - import shared/user/email 16 15 17 16 pub const empty = Model( 18 - user_name: input.Input( 19 - value: "", 20 - placeholder: "John Doe", 21 - hint: "", 22 - label: "Full name", 23 - input_type: "text", 24 - id: "input-full-name", 25 - is_valid: True, 26 - ), 27 - user_email: input.Input( 28 - value: "", 29 - placeholder: "user@email.com", 30 - hint: "", 31 - label: "Email", 32 - input_type: "email", 33 - id: "input-email", 34 - is_valid: True, 35 - ), 36 - user_phone: input.Input( 37 - value: "", 38 - placeholder: "81-912345678", 39 - hint: "", 40 - label: "Phone", 41 - input_type: "phone", 42 - id: "input-phone", 43 - is_valid: True, 44 - ), 45 - user_password: input.Input( 46 - value: "", 47 - placeholder: "**********", 48 - hint: "", 49 - label: "Password", 50 - input_type: "password", 51 - id: "input-password", 52 - is_valid: True, 53 - ), 54 - user_confirm_password: input.Input( 55 - value: "", 56 - placeholder: "**********", 57 - hint: "", 58 - label: "Confirm password", 59 - input_type: "password", 60 - id: "input-confirm-password", 61 - is_valid: True, 62 - ), 17 + user_name: "", 18 + user_email: "", 19 + user_phone: "", 20 + user_password: "", 21 + user_confirm_password: "", 63 22 user_role: role.None, 64 23 loading: False, 65 24 ) 66 25 67 26 pub type Model { 68 27 Model( 69 - user_name: input.Input, 70 - user_email: input.Input, 71 - user_phone: input.Input, 72 - user_password: input.Input, 73 - user_confirm_password: input.Input, 28 + user_name: String, 29 + user_email: String, 30 + user_phone: String, 31 + user_password: String, 32 + user_confirm_password: String, 74 33 user_role: role.Role, 75 34 loading: Bool, 76 35 ) ··· 78 37 79 38 fn build_request(model: Model) -> contract.RequestBody { 80 39 contract.RequestBody( 81 - name: model.user_name.value, 40 + name: model.user_name, 82 41 role: model.user_role, 83 - email: model.user_email.value, 84 - phone: model.user_phone.value, 85 - password: model.user_password.value, 42 + email: model.user_email, 43 + phone: model.user_phone, 44 + password: model.user_password, 86 45 ) 87 46 } 88 47 ··· 103 62 pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 104 63 case msg { 105 64 UserTypedName(value:) -> { 106 - let user_name = case value { 107 - "" -> { 108 - let is_valid = False 109 - let hint = "Name cannot be empty" 110 - input.Input(..model.user_name, value:, hint:, is_valid:) 111 - } 112 - 113 - _ -> { 114 - let is_valid = True 115 - let hint = "" 116 - input.Input(..model.user_name, value:, hint:, is_valid:) 117 - } 118 - } 119 - 120 - let new = Model(..model, user_name:) 65 + let new = Model(..model, user_name: value) 121 66 #(new, effect.none()) 122 67 } 123 68 124 - UserTypedEmail(value:) -> { 125 - let user_email = case email.parse(value) { 126 - _ if value == "" -> { 127 - let is_valid = False 128 - let hint = "Email cannot be empty" 129 - input.Input(..model.user_email, value:, hint:, is_valid:) 130 - } 69 + UserTypedEmail(value:) -> #( 70 + Model(..model, user_email: value), 71 + effect.none(), 72 + ) 131 73 132 - Ok(_) -> { 133 - let is_valid = True 134 - let hint = "" 135 - input.Input(..model.user_email, value:, hint:, is_valid:) 136 - } 137 - 138 - Error(_) -> { 139 - let is_valid = False 140 - let hint = "Provide a valid email address" 141 - input.Input(..model.user_email, value:, hint:, is_valid:) 142 - } 143 - } 144 - 145 - let new = Model(..model, user_email:) 146 - #(new, effect.none()) 147 - } 148 - 149 - UserTypedPhone(value:) -> { 150 - let user_phone = case value { 151 - "" -> { 152 - let hint = "Phone cannot be empty" 153 - let is_valid = False 154 - input.Input(..model.user_phone, value:, hint:, is_valid:) 155 - } 156 - 157 - _ -> { 158 - let is_valid = True 159 - let hint = "" 160 - input.Input(..model.user_phone, value:, hint:, is_valid:) 161 - } 162 - } 163 - 164 - let new = Model(..model, user_phone:) 165 - #(new, effect.none()) 166 - } 74 + UserTypedPhone(value:) -> #( 75 + Model(..model, user_phone: value), 76 + effect.none(), 77 + ) 167 78 168 79 UserSelectedRole(selected:) -> { 169 80 let new = Model(..model, user_role: selected) 170 81 #(new, effect.none()) 171 82 } 172 83 173 - UserTypedPassword(value:) -> { 174 - let match = value == model.user_confirm_password.value 175 - 176 - let user_password = case value { 177 - "" -> { 178 - let is_valid = False 179 - let hint = "Password cannot be empty" 180 - input.Input(..model.user_password, value:, hint:, is_valid:) 181 - } 182 - 183 - value if !match -> { 184 - let hint = "Passwords must match" 185 - let is_valid = False 186 - input.Input(..model.user_password, value:, hint:, is_valid:) 187 - } 188 - 189 - _ -> { 190 - let is_valid = True 191 - let hint = "" 192 - input.Input(..model.user_password, value:, hint:, is_valid:) 193 - } 194 - } 195 - 196 - let other = case match { 197 - True -> { 198 - let hint = "" 199 - input.Input(..model.user_confirm_password, hint:, is_valid: True) 200 - } 201 - 202 - False -> model.user_confirm_password 203 - } 204 - 205 - let new = Model(..model, user_password:, user_confirm_password: other) 206 - #(new, effect.none()) 207 - } 208 - 209 - UserTypedConfirmPassword(value:) -> { 210 - let match = value == model.user_password.value 84 + UserTypedPassword(value:) -> #( 85 + Model(..model, user_password: value), 86 + effect.none(), 87 + ) 211 88 212 - let user_confirm_password = case value { 213 - "" -> { 214 - let is_valid = False 215 - let hint = "Password cannot be empty" 216 - input.Input(..model.user_confirm_password, value:, hint:, is_valid:) 217 - } 218 - 219 - value if !match -> { 220 - let hint = "Passwords must match" 221 - let is_valid = False 222 - input.Input(..model.user_confirm_password, value:, hint:, is_valid:) 223 - } 224 - 225 - _ -> { 226 - let is_valid = True 227 - let hint = "" 228 - input.Input(..model.user_confirm_password, value:, hint:, is_valid:) 229 - } 230 - } 231 - 232 - let other = case match { 233 - True -> { 234 - let hint = "" 235 - input.Input(..model.user_password, hint:, is_valid: True) 236 - } 237 - 238 - False -> model.user_password 239 - } 240 - 241 - let new = Model(..model, user_confirm_password:, user_password: other) 242 - #(new, effect.none()) 243 - } 89 + UserTypedConfirmPassword(value:) -> #( 90 + Model(..model, user_confirm_password: value), 91 + effect.none(), 92 + ) 244 93 245 94 UserClickedSubmit -> { 246 95 let body = ··· 267 116 } 268 117 } 269 118 119 + pub fn view_input( 120 + value value: String, 121 + label label: String, 122 + placeholder placeholder: String, 123 + input_type input_type: String, 124 + on_input on_input: fn(String) -> Msg, 125 + ) -> element.Element(Msg) { 126 + let input_attributes = [ 127 + event.on_input(on_input), 128 + 129 + attr.placeholder(placeholder), 130 + attr.value(value), 131 + attr.type_(input_type), 132 + ] 133 + 134 + html.label([], [ 135 + html.text(label), 136 + html.input(input_attributes), 137 + ]) 138 + } 139 + 270 140 pub fn view(_session: session.Session, model: Model) -> element.Element(Msg) { 271 141 let attributes = [ 272 142 class(header.offset), ··· 282 152 card.content([], [ 283 153 view_fieldset([ 284 154 // Full name 285 - input.view(field: model.user_name, on_input: UserTypedName), 155 + view_input( 156 + value: model.user_name, 157 + label: "Full name", 158 + placeholder: "John Doe", 159 + input_type: "text", 160 + on_input: UserTypedName, 161 + ), 286 162 287 163 // Email 288 - input.view(field: model.user_email, on_input: UserTypedEmail), 164 + view_input( 165 + value: model.user_email, 166 + label: "Email", 167 + placeholder: "user@email.com", 168 + input_type: "email", 169 + on_input: UserTypedEmail, 170 + ), 289 171 290 172 // Phone 291 - input.view(field: model.user_phone, on_input: UserTypedPhone), 173 + view_input( 174 + value: model.user_phone, 175 + label: "Phone", 176 + placeholder: "8191234567", 177 + input_type: "phone", 178 + on_input: UserTypedPhone, 179 + ), 292 180 293 181 // Password 294 - input.view(field: model.user_password, on_input: UserTypedPassword), 182 + view_input( 183 + value: model.user_password, 184 + label: "Password", 185 + placeholder: "********", 186 + input_type: "password", 187 + on_input: UserTypedPassword, 188 + ), 295 189 296 190 // Confirm password 297 - input.view( 298 - field: model.user_confirm_password, 191 + view_input( 192 + value: model.user_confirm_password, 193 + label: "Confirm Password", 194 + placeholder: "********", 195 + input_type: "password", 299 196 on_input: UserTypedConfirmPassword, 300 197 ), 301 198 ··· 321 218 model.user_confirm_password, 322 219 ] 323 220 324 - let is_invalid = fn(field: input.Input) { !field.is_valid } 325 - let disabled = list.any(required_fields, is_invalid) 221 + let disabled = list.any(required_fields, string.is_empty) 326 222 327 223 let cursor_style = case model.loading { 328 224 True -> class("hover:cursor-progress")
-53
client/src/client/ui/input.gleam
··· 1 - import glaze/oat/form 2 - import lustre/attribute.{class} as attr 3 - import lustre/element 4 - import lustre/element/html 5 - import lustre/event 6 - 7 - /// Text input 8 - pub type Input { 9 - Input( 10 - hint: String, 11 - id: String, 12 - input_type: String, 13 - is_valid: Bool, 14 - label: String, 15 - placeholder: String, 16 - value: String, 17 - ) 18 - } 19 - 20 - pub fn view( 21 - field field: Input, 22 - on_input on_input: fn(String) -> a, 23 - ) -> element.Element(a) { 24 - let error_style = case field.is_valid { 25 - False -> class("error") 26 - True -> attr.none() 27 - } 28 - 29 - let invalid_attr = case field.is_valid { 30 - True -> [attr.none()] 31 - False -> [form.invalid(), form.field_error()] 32 - } 33 - 34 - let input_attributes = [ 35 - event.on_input(on_input), 36 - 37 - attr.placeholder(field.placeholder), 38 - attr.value(field.value), 39 - attr.type_(field.input_type), 40 - ] 41 - 42 - let hint_attr = [ 43 - attr.id(field.id <> "-hint"), 44 - form.hint(), 45 - error_style, 46 - ] 47 - 48 - html.label(invalid_attr, [ 49 - html.text(field.label), 50 - html.input(input_attributes), 51 - html.small(hint_attr, [html.text(field.hint)]), 52 - ]) 53 - }
-16
client/src/client/ui/user_card.gleam
··· 1 - import glaze/oat/card 2 - import lustre/attribute.{class} 3 - import lustre/element 4 - import lustre/element/html 5 - import lustre/event 6 - import shared/user.{type User} 7 - 8 - pub fn view(user: User, on_click: fn(User) -> a) -> element.Element(a) { 9 - let on_click = event.on_click(on_click(user)) 10 - let hover_style = class("hover:cursor-pointer hover:border-accent") 11 - 12 - card.card([on_click, hover_style, class("flex flex-col gap-1 p-2")], [ 13 - card.header([], [html.strong([], [html.text(user.full_name)])]), 14 - card.content([], [html.div([], [html.small([], [html.text(user.email)])])]), 15 - ]) 16 - }
+30
client/src/icon.gleam
··· 226 226 ], 227 227 ) 228 228 } 229 + 230 + pub fn user_search(attributes: List(Attribute(a))) { 231 + svg.svg( 232 + [ 233 + attribute("stroke-linejoin", "round"), 234 + attribute("stroke-linecap", "round"), 235 + attribute("stroke-width", "2"), 236 + attribute("stroke", "currentColor"), 237 + attribute("fill", "none"), 238 + attribute("viewBox", "0 0 24 24"), 239 + attribute("height", "24"), 240 + attribute("width", "24"), 241 + ..attributes 242 + ], 243 + [ 244 + svg.circle([ 245 + attribute("r", "4"), 246 + attribute("cy", "7"), 247 + attribute("cx", "10"), 248 + ]), 249 + svg.path([attribute("d", "M10.3 15H7a4 4 0 0 0-4 4v2")]), 250 + svg.circle([ 251 + attribute("r", "3"), 252 + attribute("cy", "17"), 253 + attribute("cx", "17"), 254 + ]), 255 + svg.path([attribute("d", "m21 21-1.9-1.9")]), 256 + ], 257 + ) 258 + }