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