Skip to content

Commit 189973f

Browse files
author
lukwol
committed
Edit task page
1 parent d41f914 commit 189973f

8 files changed

Lines changed: 334 additions & 30 deletions

File tree

client/src/api.gleam

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import error.{
55
import gleam/bool
66
import gleam/dynamic/decode.{type Decoder}
77
import gleam/fetch
8-
import gleam/http.{Get, Post}
8+
import gleam/http.{Delete, Get, Patch, Post}
99
import gleam/http/request.{type Request}
1010
import gleam/javascript/promise.{type Promise}
1111
import gleam/result
@@ -30,6 +30,34 @@ pub fn post(
3030
|> execute(expect: 201, decoder:)
3131
}
3232

33+
pub fn patch(
34+
path: String,
35+
decoder: Decoder(a),
36+
json body: String,
37+
) -> Promise(Result(a, ApiError)) {
38+
use req <- with_json_request(path)
39+
req
40+
|> request.set_method(Patch)
41+
|> request.set_header("content-type", "application/json")
42+
|> request.set_body(body)
43+
|> execute(expect: 200, decoder:)
44+
}
45+
46+
pub fn delete(path: String) -> Promise(Result(Nil, ApiError)) {
47+
use request <- with_json_request(path)
48+
request
49+
|> request.set_method(Delete)
50+
|> fetch.send
51+
|> promise.map(result.map_error(_, FetchError))
52+
|> promise.map_try(fn(response) {
53+
use <- bool.guard(
54+
response.status != 204,
55+
Error(UnexpectedStatus(response.status)),
56+
)
57+
Ok(Nil)
58+
})
59+
}
60+
3361
fn api_base_url() -> String {
3462
browser.window_location_origin()
3563
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import gleam/option.{type Option, None, Some}
2+
import lustre/attribute
3+
import lustre/element.{type Element}
4+
import lustre/element/html
5+
import lustre/event
6+
7+
pub type Msg {
8+
UserUpdatedName(String)
9+
UserUpdatedDescription(String)
10+
UserUpdatedCompleted(Bool)
11+
}
12+
13+
pub fn view(
14+
name: String,
15+
description: String,
16+
completed: Option(Bool),
17+
) -> Element(Msg) {
18+
html.div([], [
19+
html.div([], [
20+
html.label([], [element.text("Name")]),
21+
html.input([
22+
attribute.type_("text"),
23+
attribute.placeholder("Task name"),
24+
attribute.value(name),
25+
event.on_input(UserUpdatedName),
26+
]),
27+
]),
28+
html.div([], [
29+
html.label([], [element.text("Description")]),
30+
html.textarea(
31+
[
32+
attribute.placeholder("Optional description"),
33+
event.on_input(UserUpdatedDescription),
34+
],
35+
description,
36+
),
37+
]),
38+
case completed {
39+
None -> element.none()
40+
Some(value) ->
41+
html.label([], [
42+
html.input([
43+
attribute.type_("checkbox"),
44+
attribute.checked(value),
45+
event.on_check(UserUpdatedCompleted),
46+
]),
47+
element.text("Completed"),
48+
])
49+
},
50+
])
51+
}

client/src/page/edit_task.gleam

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import browser
2+
import component/task_form.{
3+
UserUpdatedCompleted, UserUpdatedDescription, UserUpdatedName,
4+
}
5+
import error.{type ApiError}
6+
import gleam/javascript/promise
7+
import gleam/option.{type Option, None, Some}
8+
import lustre/attribute
9+
import lustre/effect.{type Effect}
10+
import lustre/element.{type Element}
11+
import lustre/element/html
12+
import lustre/event
13+
import modem
14+
import route
15+
import service/task_service
16+
import task.{type Task, Task}
17+
18+
pub type Model {
19+
Model(task: Task, loading: Bool, submitting: Bool, error: Option(String))
20+
}
21+
22+
pub type Msg {
23+
FormMsg(task_form.Msg)
24+
UserSubmittedForm
25+
UserClickedDelete
26+
UserClickedBack
27+
ApiReturnedTask(Result(Task, ApiError))
28+
ApiUpdatedTask(Result(Task, ApiError))
29+
ApiDeletedTask(Result(Nil, ApiError))
30+
}
31+
32+
pub fn init(task_id: Int) -> #(Model, Effect(Msg)) {
33+
#(
34+
Model(
35+
task: Task(id: task_id, name: "", description: "", completed: False),
36+
loading: True,
37+
submitting: False,
38+
error: None,
39+
),
40+
fetch_task(task_id),
41+
)
42+
}
43+
44+
pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
45+
case msg {
46+
FormMsg(UserUpdatedName(name)) -> #(
47+
Model(..model, task: Task(..model.task, name:)),
48+
effect.none(),
49+
)
50+
FormMsg(UserUpdatedDescription(description)) -> #(
51+
Model(..model, task: Task(..model.task, description:)),
52+
effect.none(),
53+
)
54+
FormMsg(UserUpdatedCompleted(completed)) -> #(
55+
Model(..model, task: Task(..model.task, completed:)),
56+
effect.none(),
57+
)
58+
UserSubmittedForm ->
59+
case model.task.name {
60+
"" -> #(Model(..model, error: Some("Name is required")), effect.none())
61+
_ -> #(
62+
Model(..model, submitting: True, error: None),
63+
patch_task(model.task),
64+
)
65+
}
66+
UserClickedDelete -> #(
67+
Model(..model, submitting: True),
68+
delete_task(model.task.id),
69+
)
70+
UserClickedBack -> #(model, effect.from(fn(_) { browser.history_back() }))
71+
ApiReturnedTask(Ok(task)) -> #(
72+
Model(..model, task:, loading: False),
73+
effect.none(),
74+
)
75+
ApiReturnedTask(Error(err)) -> #(
76+
Model(..model, loading: False, error: Some(error.message(err))),
77+
effect.none(),
78+
)
79+
ApiUpdatedTask(Ok(_)) -> #(
80+
model,
81+
modem.push(route.to_path(route.Tasks), None, None),
82+
)
83+
ApiUpdatedTask(Error(err)) -> #(
84+
Model(..model, submitting: False, error: Some(error.message(err))),
85+
effect.none(),
86+
)
87+
ApiDeletedTask(Ok(_)) -> #(
88+
model,
89+
modem.push(route.to_path(route.Tasks), None, None),
90+
)
91+
ApiDeletedTask(Error(err)) -> #(
92+
Model(..model, submitting: False, error: Some(error.message(err))),
93+
effect.none(),
94+
)
95+
}
96+
}
97+
98+
pub fn view(model: Model) -> Element(Msg) {
99+
case model.loading {
100+
True -> html.p([], [element.text("Loading...")])
101+
False ->
102+
html.div([], [
103+
html.h1([], [element.text("Edit Task")]),
104+
case model.error {
105+
None -> element.none()
106+
Some(err) -> html.p([], [element.text(err)])
107+
},
108+
task_form.view(
109+
model.task.name,
110+
model.task.description,
111+
Some(model.task.completed),
112+
)
113+
|> element.map(FormMsg),
114+
html.div([], [
115+
html.button(
116+
[
117+
attribute.disabled(model.submitting),
118+
event.on_click(UserSubmittedForm),
119+
],
120+
[
121+
element.text(case model.submitting {
122+
True -> "Saving..."
123+
False -> "Save"
124+
}),
125+
],
126+
),
127+
html.button(
128+
[
129+
attribute.disabled(model.submitting),
130+
event.on_click(UserClickedDelete),
131+
],
132+
[element.text("Delete")],
133+
),
134+
html.button([event.on_click(UserClickedBack)], [element.text("Back")]),
135+
]),
136+
])
137+
}
138+
}
139+
140+
fn fetch_task(task_id: Int) -> Effect(Msg) {
141+
use dispatch <- effect.from
142+
task_service.fetch_task(task_id)
143+
|> promise.map(ApiReturnedTask)
144+
|> promise.tap(dispatch)
145+
Nil
146+
}
147+
148+
fn patch_task(task: Task) -> Effect(Msg) {
149+
use dispatch <- effect.from
150+
task_service.patch_task(task)
151+
|> promise.map(ApiUpdatedTask)
152+
|> promise.tap(dispatch)
153+
Nil
154+
}
155+
156+
fn delete_task(task_id: Int) -> Effect(Msg) {
157+
use dispatch <- effect.from
158+
task_service.delete_task(task_id)
159+
|> promise.map(ApiDeletedTask)
160+
|> promise.tap(dispatch)
161+
Nil
162+
}

client/src/page/new_task.gleam

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import browser
2+
import component/task_form.{
3+
UserUpdatedCompleted, UserUpdatedDescription, UserUpdatedName,
4+
}
25
import error.{type ApiError}
36
import gleam/javascript/promise
47
import gleam/option.{type Option, None, Some}
@@ -22,10 +25,9 @@ pub type Model {
2225
}
2326

2427
pub type Msg {
25-
UserUpdatedName(String)
26-
UserUpdatedDescription(String)
27-
UserClickedBack
28+
FormMsg(task_form.Msg)
2829
UserSubmittedForm
30+
UserClickedBack
2931
ApiCreatedTask(Result(Task, ApiError))
3032
}
3133

@@ -38,12 +40,12 @@ pub fn init() -> #(Model, Effect(Msg)) {
3840

3941
pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
4042
case msg {
41-
UserUpdatedName(name) -> #(Model(..model, name:), effect.none())
42-
UserUpdatedDescription(description) -> #(
43+
FormMsg(UserUpdatedName(name)) -> #(Model(..model, name:), effect.none())
44+
FormMsg(UserUpdatedDescription(description)) -> #(
4345
Model(..model, description:),
4446
effect.none(),
4547
)
46-
UserClickedBack -> #(model, effect.from(fn(_) { browser.history_back() }))
48+
FormMsg(UserUpdatedCompleted(_)) -> #(model, effect.none())
4749
UserSubmittedForm ->
4850
case model.name {
4951
"" -> #(Model(..model, error: Some("Name is required")), effect.none())
@@ -52,6 +54,7 @@ pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
5254
post_task(model.name, model.description),
5355
)
5456
}
57+
UserClickedBack -> #(model, effect.from(fn(_) { browser.history_back() }))
5558
ApiCreatedTask(Ok(_)) -> #(
5659
model,
5760
modem.push(route.to_path(route.Tasks), None, None),
@@ -70,25 +73,8 @@ pub fn view(model: Model) -> Element(Msg) {
7073
None -> element.none()
7174
Some(err) -> html.p([], [element.text(err)])
7275
},
73-
html.div([], [
74-
html.label([], [element.text("Name")]),
75-
html.input([
76-
attribute.type_("text"),
77-
attribute.placeholder("Task name"),
78-
attribute.value(model.name),
79-
event.on_input(UserUpdatedName),
80-
]),
81-
]),
82-
html.div([], [
83-
html.label([], [element.text("Description")]),
84-
html.textarea(
85-
[
86-
attribute.placeholder("Optional description"),
87-
event.on_input(UserUpdatedDescription),
88-
],
89-
model.description,
90-
),
91-
]),
76+
task_form.view(model.name, model.description, None)
77+
|> element.map(FormMsg),
9278
html.div([], [
9379
html.button(
9480
[

0 commit comments

Comments
 (0)