Household planning and management software meant to organise and streamline neurodivergent households.
0

Configure Feed

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

Add a lil unlocking screen

MLC Bloeiman (Jan 14, 2026, 5:22 PM +0100) 08dac4d2 a997cbe2

+131 -46
+129 -46
sweetnhouse_dashboard/src/sweetnhouse_dashboard.gleam
··· 7 7 import gleam/int 8 8 import gleam/list 9 9 import gleam/option.{type Option, None} 10 + import gleam/order 10 11 import gleam/result 11 12 import gleam/string 12 13 import gleam/uri ··· 50 51 /// Domain specific models 51 52 /// E.g. models for specific features of the dashboard 52 53 type DomainModel { 53 - LockScreenModel(Form(PinCodeFormData)) 54 + /// Model for the lock screen 55 + LockScreenModel(LockScreenModel) 56 + } 57 + 58 + type LockScreenModel { 59 + LockscreenForm(Form(PinCodeFormData)) 60 + LockscreenProcessing 54 61 } 55 62 56 63 /// A pin code consists of 6 digits ··· 105 112 locked: None, 106 113 enabled_tabs: [], 107 114 ), 108 - domain: LockScreenModel(new_unlock_form()), 115 + domain: LockScreenModel(LockscreenForm(new_unlock_form())), 109 116 ) 110 117 let effect = fetch_translations(on_response: ApiReturnedTranslations) 111 118 ··· 129 136 ApiReturnedTranslations(Result(Translations, rsvp.Error)) 130 137 LockScreenEnteredPin(Result(PinCodeFormData, Form(PinCodeFormData))) 131 138 SetLanguage(Language) 139 + LockNow 132 140 } 133 141 134 142 fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { 135 143 case msg { 144 + LockNow -> #( 145 + Model( 146 + // ..model, 147 + global: GlobalModel(..model.global, locked: None), 148 + domain: LockScreenModel(LockscreenForm(new_unlock_form())), 149 + ), 150 + effect.none(), 151 + ) 136 152 LockScreenEnteredPin(pin_result) -> { 137 153 case pin_result { 138 154 Error(form_with_errors) -> { 139 155 let new_model = 140 - Model(..model, domain: LockScreenModel(form_with_errors)) 156 + Model( 157 + ..model, 158 + domain: LockScreenModel(LockscreenForm(form_with_errors)), 159 + ) 141 160 #(new_model, effect.none()) 142 161 } 143 162 // TODO: Handle successful unlock 144 - Ok(_) -> #(model, effect.none()) 163 + Ok(form) -> { 164 + let new_model = 165 + Model(..model, domain: LockScreenModel(LockscreenProcessing)) 166 + #(new_model, effect.none()) 167 + } 145 168 } 146 169 } 147 170 SetLanguage(new_lang) -> { ··· 192 215 // We use special sentinel values to indicate specific errors, positioned 193 216 // in the first element of the tuple. 194 217 let #(a, _, _, _, _, _) = pincode 195 - case a { 196 - -10 -> Error("invalid_pin_message_digitsonly") 197 - -25 -> Error("invalid_pin_message_length") 218 + case a |> int.compare(0) { 219 + order.Lt if a == -10 -> Error("invalid_pin_message_digitsonly") 220 + order.Lt if a == -25 -> Error("invalid_pin_message_length") 198 221 _ -> Ok(pincode) 199 222 } 200 223 } ··· 222 245 ), 223 246 ], 224 247 case model.domain { 225 - LockScreenModel(form) -> [view_lockscreen(form, lang)] 248 + LockScreenModel(inner) -> [ 249 + view_lockscreen(inner, lang), 250 + ] 226 251 }, 227 252 ), 228 253 ]) 229 254 } 230 255 231 256 fn view_lockscreen( 232 - form: Form(PinCodeFormData), 257 + inner: LockScreenModel, 233 258 lang: fn(String) -> Element(Msg), 234 259 ) -> Element(Msg) { 235 - let handle_submit = fn(values) { 236 - form |> form.add_values(values) |> form.run |> LockScreenEnteredPin 260 + case inner { 261 + LockscreenForm(form) -> { 262 + let handle_submit = fn(values) { 263 + form |> form.add_values(values) |> form.run |> LockScreenEnteredPin 264 + } 265 + html.form( 266 + [ 267 + attribute.class( 268 + "p-8 w-[65VW] border rounded-2xl shadow-lg space-y-4 bg-base-100 border-base-300 text-base-content", 269 + ), 270 + event.on_submit(handle_submit), 271 + ], 272 + [ 273 + html.h1( 274 + [attribute.class("text-lg md:text-2xl font-medium text-primary")], 275 + [ 276 + lang("lockscreen_title"), 277 + ], 278 + ), 279 + view_input( 280 + form, 281 + is: "password", 282 + name: "pincode", 283 + label: "enter_pin_placeholder", 284 + lang: lang, 285 + ), 286 + html.div([attribute.class("flex justify-end")], [ 287 + html.button( 288 + [ 289 + attribute.class("text-white text-sm font-bold"), 290 + attribute.class( 291 + "px-4 py-2 bg-info text-info-content rounded-lg", 292 + ), 293 + attribute.class( 294 + "hover:bg-secondary hover:text-secondary-content", 295 + ), 296 + attribute.class( 297 + "focus:outline-2 focus:outline-offset-2 focus:bg-secondary focus:text-secondary-content", 298 + ), 299 + ], 300 + [lang("lockscreen_unlock_button")], 301 + ), 302 + ]), 303 + ], 304 + ) 305 + } 306 + LockscreenProcessing -> { 307 + html.div( 308 + [ 309 + attribute.class( 310 + "p-8 w-[65VW] border rounded-2xl shadow-lg space-y-4 bg-base-100 border-base-300 text-base-content", 311 + ), 312 + ], 313 + [ 314 + html.h1( 315 + [attribute.class("text-lg md:text-2xl font-medium text-primary")], 316 + [ 317 + lang("lockscreen_processing"), 318 + html.span( 319 + [attribute.class("loading loading-spinner loading-xs ml-2")], 320 + [], 321 + ), 322 + ], 323 + ), 324 + ], 325 + ) 326 + } 237 327 } 238 - 239 - html.form( 240 - [ 241 - attribute.class( 242 - "p-8 w-[65VW] border rounded-2xl shadow-lg space-y-4 bg-base-100 border-base-300 text-base-content", 243 - ), 244 - event.on_submit(handle_submit), 245 - ], 246 - [ 247 - html.h1([attribute.class("text-2xl font-medium text-primary")], [ 248 - lang("lockscreen_title"), 249 - ]), 250 - view_input( 251 - form, 252 - is: "password", 253 - name: "pincode", 254 - label: "enter_pin_placeholder", 255 - lang: lang, 256 - ), 257 - html.div([attribute.class("flex justify-end")], [ 258 - html.button( 259 - [ 260 - attribute.class("text-white text-sm font-bold"), 261 - attribute.class("px-4 py-2 bg-info text-info-content rounded-lg"), 262 - attribute.class("hover:bg-secondary hover:text-secondary-content"), 263 - attribute.class( 264 - "focus:outline-2 focus:outline-offset-2 focus:bg-secondary focus:text-secondary-content", 265 - ), 266 - ], 267 - [lang("lockscreen_unlock_button")], 268 - ), 269 - ]), 270 - ], 271 - ) 272 328 } 273 329 274 330 fn view_input( ··· 358 414 html.div([attribute.class("navbar-end")], [ 359 415 html.a( 360 416 [ 361 - attribute.class("btn"), 417 + attribute.class("btn hidden md:block h-full"), 418 + event.on_click(LockNow), 362 419 attribute.disabled(model.global.locked |> option.is_none), 363 420 ], 364 421 [lang("btn_lock")], 422 + ), 423 + html.a( 424 + [ 425 + attribute.class("btn block md:hidden btn h-full"), 426 + event.on_click(LockNow), 427 + attribute.disabled(model.global.locked |> option.is_none), 428 + ], 429 + [ 430 + svg.svg( 431 + [ 432 + attribute.attribute("height", "24"), 433 + attribute.attribute("width", "24"), 434 + attribute.attribute("viewBox", "0 0 24 24"), 435 + attribute.attribute("xmlns", "http://www.w3.org/2000/svg"), 436 + ], 437 + [ 438 + svg.path([ 439 + attribute.attribute( 440 + "d", 441 + "M12 2a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-1V7a5 5 0 0 0-5-5zm3 8H9V7a3 3 0 0 1 6 0v3z", 442 + ), 443 + attribute.attribute("fill", "currentColor"), 444 + ]), 445 + ], 446 + ), 447 + ], 365 448 ), 366 449 ]), 367 450 ])
+2
sweetnhouse_server/priv/static/translations.json
··· 7 7 "invalid_pin_message_digitsonly": "PIN code must consist of digits only.", 8 8 "invalid_pin_message_length": "PIN code must be 6 digits long.", 9 9 "btn_lock": "Lock", 10 + "lockscreen_processing": "Unlocking...", 10 11 "menu_language": "Language", 11 12 "menu_system_info": "System Info", 12 13 "lang_en_gb": "🇬🇧 English (UK)", ··· 20 21 "invalid_pin_message_digitsonly": "Pincode mag alleen uit cijfers bestaan.", 21 22 "invalid_pin_message_length": "Pincode moet 6 cijfers lang zijn.", 22 23 "btn_lock": "Vergrendelen", 24 + "lockscreen_processing": "Ontgrendelen...", 23 25 "menu_language": "Taal", 24 26 "menu_system_info": "Systeeminfo", 25 27 "lang_en_gb": "🇬🇧 Engels (VK)",