From 3d735e00bc3fa1cbf622daf53562c0e6dfb6c93e Mon Sep 17 00:00:00 2001 From: Taran Nathan Date: Thu, 24 Sep 2026 14:38:10 -0700 Subject: [PATCH] works --- client/src/client.gleam | 263 +++++++-- server/README.md | 5 +- server/src/server.gleam | 30 +- server/src/sql.gleam | 14 + server/static/client.js | 1194 ++++++++++++++++++++------------------- shared/src/shared.gleam | 6 +- 6 files changed, 869 insertions(+), 643 deletions(-) diff --git a/client/src/client.gleam b/client/src/client.gleam index dc4f0a7..c08fdf3 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -1,4 +1,5 @@ import gleam/int +import gleam/json import gleam/list import gleam/option.{type Option, None, Some} import gleam/result @@ -26,6 +27,7 @@ type Model { error: Option(String), username: String, password: String, + invite_password: String, project_name: String, project_description: String, task_title: String, @@ -54,6 +56,9 @@ pub type Message { TaskAssigneeChanged(String) TaskAssigneesChanged(String) TaskDueDateChanged(String) + InvitePasswordChanged(String) + DeleteTask(Int) + DeleteProject(Int) LogOut Nothing } @@ -65,7 +70,6 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { _ -> "dashboard" } - echo page let model = Model( page:, @@ -73,6 +77,7 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { error: None, username: "", password: "", + invite_password: "", project_name: "", project_description: "", task_title: "", @@ -83,28 +88,26 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { ) case page { - // "dashboard" -> - // #( - // model, - // rsvp.get( - // "/api/dashboard", - // rsvp.expect_json(shared.dashboard_decoder(), DashboardLoaded), - // ), - // ) - _ -> - //#(model, effect.none()) - auth_request(model, [], "/api/login", "/") + "dashboard" | "login" -> #( + model, + rsvp.get( + "/api/dashboard", + rsvp.expect_json(shared.dashboard_decoder(), DashboardLoaded), + ), + ) + _ -> #(model, effect.none()) } } fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { case message { DashboardLoaded(Ok(value)) -> { - echo "dashload good" - #(Model(..model, dashboard: Some(value), error: None), effect.none()) + #( + Model(..model, page: "dashboard", dashboard: Some(value), error: None), + effect.none(), + ) } DashboardLoaded(Error(_)) -> { - echo "dashload err" #( Model( ..model, @@ -116,6 +119,10 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { } UsernameChanged(value) -> #(Model(..model, username: value), effect.none()) PasswordChanged(value) -> #(Model(..model, password: value), effect.none()) + InvitePasswordChanged(value) -> #( + Model(..model, invite_password: value), + effect.none(), + ) ProjectNameChanged(value) -> #( Model(..model, project_name: value), effect.none(), @@ -197,6 +204,26 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { ), ) } + DeleteTask(task_id) -> #( + model, + rsvp.delete( + "/api/tasks/" <> int.to_string(task_id), + json.object([]), + rsvp.expect_any_response(fn(result) { + Saved(result |> result.map(fn(response) { response.body })) + }), + ), + ) + DeleteProject(project_id) -> #( + model, + rsvp.delete( + "/api/projects/" <> int.to_string(project_id), + json.object([]), + rsvp.expect_any_response(fn(result) { + Saved(result |> result.map(fn(response) { response.body })) + }), + ), + ) Authenticated(Ok(_)) -> #( Model(..model, page: "dashboard"), rsvp.get( @@ -218,6 +245,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { task_assignee: "", task_assignees: "", task_due_date: "", + invite_password: "", ), rsvp.get( "/api/dashboard", @@ -243,7 +271,11 @@ fn auth_request( _redirect: String, ) -> #(Model, Effect(Message)) { let body = - shared.Credentials(field(fields, "username"), field(fields, "password")) + shared.Credentials( + field(fields, "username"), + field(fields, "password"), + if_empty_invite(fields), + ) |> shared.credentials_json #( model, @@ -261,7 +293,7 @@ fn view(model: Model) -> Element(Message) { case model.page { "login" -> auth_view(model, "Welcome back", "Log in", LoginSubmitted) "register" -> - auth_view( + creat_account_view( model, "Create your account", "Create account", @@ -276,6 +308,44 @@ fn auth_view( title: String, submit: String, on_submit: fn(List(#(String, String))) -> Message, +) -> Element(Message) { + let link = case submit == "Log in" { + True -> "/register" + False -> "/login" + } + let link_text = case submit == "Log in" { + True -> "Need an account?" + False -> "Already registered?" + } + html.main([attribute.class("auth")], [ + styles(), + html.text("Team 972 · IRON CLAW"), + html.h1([], [html.text(title)]), + html.p([], [html.text("google sheets, more like shmoogle shits")]), + error_view(model.error), + html.form([event.on_submit(on_submit)], [ + html.input([ + attribute.name("username"), + attribute.placeholder("Username"), + event.on_input(UsernameChanged), + ]), + html.input([ + attribute.name("password"), + attribute.type_("password"), + attribute.placeholder("Password"), + event.on_input(PasswordChanged), + ]), + html.button([], [html.text(submit)]), + ]), + html.a([attribute.href(link)], [html.text(link_text)]), + ]) +} + +fn creat_account_view( + model: Model, + title: String, + submit: String, + on_submit: fn(List(#(String, String))) -> Message, ) -> Element(Message) { let link = case submit == "Log in" { True -> "/register" @@ -297,6 +367,12 @@ fn auth_view( attribute.placeholder("Username"), event.on_input(UsernameChanged), ]), + html.input([ + attribute.name("invite_password"), + attribute.type_("password"), + attribute.placeholder("Team signup password"), + event.on_input(InvitePasswordChanged), + ]), html.input([ attribute.name("password"), attribute.type_("password"), @@ -325,7 +401,7 @@ fn dashboard_view(model: Model) -> Element(Message) { html.header([], [ html.div([], [ html.text("FRC 972 · IRON CLAW"), - html.h1([], [html.text("Team board")]), + html.h1([], [html.text("project board")]), ]), html.button([event.on_click(LogOut)], [ html.text("Sign out"), @@ -338,7 +414,7 @@ fn dashboard_view(model: Model) -> Element(Message) { html.h2([], [html.text("New project")]), html.form([event.on_submit(ProjectSubmitted)], [ text_input("name", "Project name"), - text_input("description", "What does success look like?"), + text_input("description", "Description/Goals"), html.button([], [html.text("Add project")]), ]), ]), @@ -350,6 +426,9 @@ fn dashboard_view(model: Model) -> Element(Message) { } fn project_view(_model: Model, project: shared.Project) -> Element(Message) { + let backlog = list.filter(project.tasks, fn(task) { task.status == "todo" }) + let doing = list.filter(project.tasks, fn(task) { task.status == "doing" }) + let done = list.filter(project.tasks, fn(task) { task.status == "done" }) html.article( [attribute.class("panel")], list.append( @@ -357,54 +436,126 @@ fn project_view(_model: Model, project: shared.Project) -> Element(Message) { html.text("PROJECT"), html.h2([], [html.text(project.name)]), html.p([], [html.text(project.description)]), + html.button([event.on_click(DeleteProject(project.id))], [ + html.text("Delete project"), + ]), ], - list.append(list.map(project.tasks, task_view), [ - html.form( - [event.on_submit(fn(fields) { TaskSubmitted(project.id, fields) })], - [ - text_input("title", "Task title"), - text_input("assignee", "Owner"), - text_input("assignees", "Assignees"), - text_input("due_date", "Due date"), - text_input("description", "Details"), - html.button([], [html.text("Add task")]), - ], - ), - ]), + list.append( + [ + html.div([attribute.class("kanban")], [ + kanban_column("To do", backlog), + kanban_column("In progress", doing), + kanban_column("Done", done), + ]), + ], + [ + html.form( + [event.on_submit(fn(fields) { TaskSubmitted(project.id, fields) })], + [ + text_input("title", "Task title"), + text_input("assignee", "Owner"), + text_input("assignees", "Assignees"), + text_input("due_date", "Due (MM/DD/YY)"), + text_input("description", "Details"), + html.button([], [html.text("Add task")]), + ], + ), + ], + ), + ), + ) +} + +fn kanban_column(title: String, tasks: List(shared.Task)) -> Element(Message) { + html.section( + [attribute.class("kanban-column")], + list.append( + [ + html.h3([], [ + html.text(title <> " · " <> int.to_string(list.length(tasks))), + ]), + ], + list.map(tasks, task_view), ), ) } fn task_view(task: shared.Task) -> Element(Message) { - let next = case task.status { - "todo" -> "doing" - "doing" -> "done" - _ -> "todo" - } html.div([attribute.class("task")], [ - html.div([], [ - html.strong([], [html.text(task.title)]), - html.p([], [html.text(task.description)]), - ]), - html.div([attribute.class("task-meta")], [ - html.span([], [html.text(task.status)]), - html.text("Owner: " <> task.assignee), - html.text("Assignees: " <> task.assignees), - html.form( - [event.on_submit(fn(fields) { StatusSubmitted(task.id, fields) })], + html.div([attribute.class("task-header")], [ + html.strong([attribute.class("task-title")], [html.text(task.title)]), + html.button( + [event.on_click(DeleteTask(task.id)), attribute.class("delete-task")], [ - html.input([ - attribute.type_("hidden"), - attribute.name("status"), - attribute.value(next), - ]), - html.button([], [html.text("Move to " <> next)]), + html.text("Delete"), ], ), ]), + html.p([attribute.class("task-description")], [html.text(task.description)]), + html.div([attribute.class("task-details")], [ + html.div([attribute.class("task-person")], [ + html.small([], [html.text("Owner")]), + html.text(if_blank(task.assignee, "Unassigned")), + ]), + html.div([attribute.class("task-person")], [ + html.small([], [html.text("Assignees")]), + html.text(if_blank(task.assignees, "None")), + ]), + html.div([attribute.class("task-due")], [ + html.small([], [html.text("Due")]), + html.text(case task.due_date { + "" -> "No due date" + date -> date + }), + ]), + ]), + html.div([attribute.class("task-footer")], [ + html.span([attribute.class("task-status")], [html.text(task.status)]), + html.div([attribute.class("task-actions")], [ + left_status_button(task), + right_status_button(task), + ]), + ]), ]) } +fn left_status_button(task: shared.Task) -> Element(Message) { + case task.status { + "doing" -> status_button(task.id, "todo", "←") + "done" -> status_button(task.id, "doing", "←") + _ -> element.none() + } +} + +fn right_status_button(task: shared.Task) -> Element(Message) { + case task.status { + "todo" -> status_button(task.id, "doing", "→") + "doing" -> status_button(task.id, "done", "→") + _ -> element.none() + } +} + +fn status_button( + task_id: Int, + status: String, + label: String, +) -> Element(Message) { + html.button( + [ + attribute.class("move-task"), + event.on_click(StatusSubmitted(task_id, [#("status", status)])), + ], + [html.text(label)], + ) +} + +fn if_blank(value: String, fallback: String) -> String { + case value == "" { + True -> fallback + False -> value + } +} + fn text_input(name: String, placeholder: String) -> Element(Message) { html.input([ attribute.name(name), @@ -415,7 +566,7 @@ fn text_input(name: String, placeholder: String) -> Element(Message) { fn styles() -> Element(Message) { html.style( [], - "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;border-radius:22px;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:#b53b24;font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:#b53b24;font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;border-radius:22px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;border-radius:18px;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.task{display:flex;justify-content:space-between;gap:20px;padding:17px 0;border-top:1px solid #eee8df}.task p{margin:5px 0 0;color:#796f64}.task-meta{display:flex;align-items:center;justify-content:flex-end;gap:9px;flex-wrap:wrap;color:#796f64;font-size:.82rem}.task-meta span{padding:5px 9px;border-radius:99px;background:#f0ebe3}.task-meta button{padding:7px 10px;font-size:.78rem}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input{min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;border-radius:10px;background:#fffcf8;color:#28231e;font:inherit}button{border:0;border-radius:10px;background:#b53b24;color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:#b53b24;font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:650px){.task{display:block}.task-meta{justify-content:flex-start;margin-top:12px}header{display:block}.hero{padding:28px}}", + "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:#b53b24;font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:#b53b24;font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.kanban{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:22px}.kanban-column{background:#f7f3ed;border:1px solid #ebe1d4;padding:13px}.kanban-column h3{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#796f64;margin:0 0 5px}.task{display:flex;flex-direction:column;gap:13px;background:#fff;border:1px solid #e8dfd4;padding:16px;margin:9px 0;box-shadow:0 4px 12px #2d24140a;min-width:0}.task-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-title{font-size:1rem;line-height:1.25;overflow-wrap:anywhere}.delete-task{flex:0 0 auto;padding:5px 8px;font-size:.68rem;background:#f0ebe3;color:#796f64}.delete-task:hover{background:#e8dfd4;color:#28231e}.task-description{margin:0;color:#796f64;line-height:1.45;overflow-wrap:anywhere}.task-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px 14px;padding-top:12px;border-top:1px solid #eee6dc;color:#4e463e;font-size:.8rem}.task-person,.task-due{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-wrap:anywhere}.task-details small{color:#a04a35;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-due{grid-column:1/-1}.task-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}.task-status{padding:5px 8px;background:#f0ebe3;color:#796f64;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-actions{display:flex;gap:6px;margin-left:auto}.task-actions form{margin:0}.move-task{width:31px;height:29px;padding:0;font-size:1.05rem;line-height:1}.move-task:hover{background:#902e1d}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input{min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;background:#fffcf8;color:#28231e;font:inherit}button{border:0;background:#b53b24;color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:#b53b24;font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:800px){.kanban{grid-template-columns:1fr 1fr}}@media(max-width:650px){.kanban{grid-template-columns:1fr}.task-details{grid-template-columns:1fr}.task-due{grid-column:auto}header{display:block}.hero{padding:28px}}", ) } @@ -430,6 +581,10 @@ fn field(fields: List(#(String, String)), name: String) -> String { list.key_find(fields, name) |> result.unwrap("") } +fn if_empty_invite(fields: List(#(String, String))) -> String { + field(fields, "invite_password") +} + fn current_path() -> String { window.self() |> window.location |> location.pathname } diff --git a/server/README.md b/server/README.md index abe3858..036b22f 100644 --- a/server/README.md +++ b/server/README.md @@ -25,7 +25,10 @@ gleam run -m lustre/dev build cp dist/client.js ../server/static/client.js ``` -Then open to create the first account. +Then open to create the first account. Account +registration requires the team signup password `iron-claw-972` in addition to +the account password. Change `signup_password` in `src/server.gleam` before +sharing the app beyond local development. Run the test suite with `gleam test`. If the database schema changes, run `gleam run -m squirrel` with the same `DATABASE_URL` to regenerate the typed diff --git a/server/src/server.gleam b/server/src/server.gleam index f7452c1..aeb249a 100644 --- a/server/src/server.gleam +++ b/server/src/server.gleam @@ -18,6 +18,8 @@ import sql import wisp.{type Request, type Response} import wisp/wisp_mist +const signup_password = "cremebrulee" + pub fn main() -> Nil { wisp.configure_logger() let assert Ok(db) = database.start() @@ -73,6 +75,9 @@ fn handle_request(db: Database, request: Request) -> Response { json_body(req, fn(body) { add_task(db, req, project_id, body) }) http.Post, ["api", "tasks", task_id, "status"] -> json_body(req, fn(body) { change_status(db, req, task_id, body) }) + http.Delete, ["api", "tasks", task_id] -> delete_task(db, req, task_id) + http.Delete, ["api", "projects", project_id] -> + delete_project(db, req, project_id) _, _ -> wisp.not_found() } } @@ -139,6 +144,7 @@ fn register(db: Database, req: Request, body: String) -> Response { case string.length(credentials.username) >= 3 && string.length(credentials.password) >= 8 + && credentials.invite_password == signup_password { True -> case @@ -156,10 +162,11 @@ fn register(db: Database, req: Request, body: String) -> Response { False -> wisp.json_response( - "{\"error\":\"username and password are required\"}", + "{\"error\":\"valid username, password, and team signup password are required\"}", 400, ) } + _ -> wisp.json_response( "{\"error\":\"username and password are required\"}", @@ -168,6 +175,26 @@ fn register(db: Database, req: Request, body: String) -> Response { } } +fn delete_task(db: Database, req: Request, task_id: String) -> Response { + case current_user(db, req), int.parse(task_id) { + Ok(_), Ok(id) -> { + let assert Ok(_) = sql.delete_task(db.connection, id) + wisp.json_response("{\"ok\":true}", 200) + } + _, _ -> wisp.json_response("{\"error\":\"unauthorized\"}", 401) + } +} + +fn delete_project(db: Database, req: Request, project_id: String) -> Response { + case current_user(db, req), int.parse(project_id) { + Ok(_), Ok(id) -> { + let assert Ok(_) = sql.delete_project(db.connection, id) + wisp.json_response("{\"ok\":true}", 200) + } + _, _ -> wisp.json_response("{\"error\":\"unauthorized\"}", 401) + } +} + fn registration_error(error: pog.QueryError) -> Response { case error { pog.ConstraintViolated(_, _, _) -> @@ -268,7 +295,6 @@ fn json_body(req: Request, next: fn(String) -> Response) -> Response { fn current_user(db: Database, req: Request) -> Result(String, Nil) { use token <- result.try(wisp.get_cookie(req, "session", wisp.Signed)) - echo token case sql.find_session(db.connection, token) { Ok(pog.Returned(_, [user_id])) -> case sql.find_username(db.connection, user_id) { diff --git a/server/src/sql.gleam b/server/src/sql.gleam index 96d6464..165e473 100644 --- a/server/src/sql.gleam +++ b/server/src/sql.gleam @@ -138,6 +138,20 @@ pub fn update_task_status(db: pog.Connection, status: String, task_id: Int) { |> pog.execute(db) } +pub fn delete_task(db: pog.Connection, task_id: Int) { + "delete from task where id = $1" + |> pog.query + |> pog.parameter(pog.int(task_id)) + |> pog.execute(db) +} + +pub fn delete_project(db: pog.Connection, project_id: Int) { + "delete from project where id = $1" + |> pog.query + |> pog.parameter(pog.int(project_id)) + |> pog.execute(db) +} + pub fn create_session(db: pog.Connection, token: String, user_id: Int) { "insert into session (token, user_id, expires_at) values ($1, $2, now() + interval '30 days')" |> pog.query diff --git a/server/static/client.js b/server/static/client.js index 537228d..1e44740 100644 --- a/server/static/client.js +++ b/server/static/client.js @@ -289,6 +289,50 @@ function makeError(variant, file, module, line, fn, message, extra) { error[k] = extra[k]; return error; } +// build/dev/javascript/gleam_stdlib/gleam/order.mjs +class Lt extends CustomType { +} +var Order$Lt$const = new Lt; +var Order$Lt = () => Order$Lt$const; +class Eq extends CustomType { +} +var Order$Eq$const = new Eq; +var Order$Eq = () => Order$Eq$const; +class Gt extends CustomType { +} +var Order$Gt$const = new Gt; +var Order$Gt = () => Order$Gt$const; + +// build/dev/javascript/gleam_stdlib/gleam/option.mjs +class Some extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +var Option$isSome = (value) => value instanceof Some; +var Option$Some$0 = (value) => value[0]; + +class None extends CustomType { +} +var Option$None$const = new None; +function to_result(option, e) { + if (option instanceof Some) { + let a = option[0]; + return new Ok(a); + } else { + return new Error2(e); + } +} +function unwrap(option, default$) { + if (option instanceof Some) { + let x = option[0]; + return x; + } else { + return default$; + } +} + // build/dev/javascript/gleam_stdlib/dict.mjs var referenceMap = /* @__PURE__ */ new WeakMap; var tempDataView = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8)); @@ -706,36 +750,6 @@ function hashbit(hash, shift) { return 1 << (hash >>> shift & mask); } -// build/dev/javascript/gleam_stdlib/gleam/option.mjs -class Some extends CustomType { - constructor($0) { - super(); - this[0] = $0; - } -} -var Option$isSome = (value) => value instanceof Some; -var Option$Some$0 = (value) => value[0]; - -class None extends CustomType { -} -var Option$None$const = new None; -function to_result(option, e) { - if (option instanceof Some) { - let a = option[0]; - return new Ok(a); - } else { - return new Error2(e); - } -} -function unwrap(option, default$) { - if (option instanceof Some) { - let x = option[0]; - return x; - } else { - return default$; - } -} - // build/dev/javascript/gleam_stdlib/gleam/dict.mjs function keys(dict) { return fold(dict, List$Empty$const, (acc, key, _) => { @@ -750,20 +764,6 @@ function delete$(dict, key) { return fromTransient(_pipe$1); } -// build/dev/javascript/gleam_stdlib/gleam/order.mjs -class Lt extends CustomType { -} -var Order$Lt$const = new Lt; -var Order$Lt = () => Order$Lt$const; -class Eq extends CustomType { -} -var Order$Eq$const = new Eq; -var Order$Eq = () => Order$Eq$const; -class Gt extends CustomType { -} -var Order$Gt$const = new Gt; -var Order$Gt = () => Order$Gt$const; - // build/dev/javascript/gleam_stdlib/gleam/list.mjs class Ascending extends CustomType { } @@ -772,6 +772,22 @@ var Sorting$Ascending$const = new Ascending; class Descending extends CustomType { } var Sorting$Descending$const = new Descending; +function length_loop(loop$list, loop$count) { + while (true) { + let list = loop$list; + let count = loop$count; + if (list instanceof Empty) { + return count; + } else { + let list$1 = list.tail; + loop$list = list$1; + loop$count = count + 1; + } + } +} +function length(list) { + return length_loop(list, 0); +} function reverse_and_prepend(loop$prefix, loop$suffix) { while (true) { let prefix = loop$prefix; @@ -789,6 +805,33 @@ function reverse_and_prepend(loop$prefix, loop$suffix) { function reverse(list) { return reverse_and_prepend(list, List$Empty$const); } +function filter_loop(loop$list, loop$fun, loop$acc) { + while (true) { + let list = loop$list; + let fun = loop$fun; + let acc = loop$acc; + if (list instanceof Empty) { + return reverse(acc); + } else { + let first$1 = list.head; + let rest$1 = list.tail; + let _block; + let $ = fun(first$1); + if ($) { + _block = prepend(first$1, acc); + } else { + _block = acc; + } + let new_acc = _block; + loop$list = rest$1; + loop$fun = fun; + loop$acc = new_acc; + } + } +} +function filter(list, predicate) { + return filter_loop(list, predicate, List$Empty$const); +} function filter_map_loop(loop$list, loop$fun, loop$acc) { while (true) { let list = loop$list; @@ -1262,47 +1305,6 @@ function each(loop$list, loop$f) { } } -// build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs -class All extends CustomType { -} -var Direction$All$const = new All; - -// build/dev/javascript/gleam_stdlib/gleam/string.mjs -class Leading extends CustomType { -} -var Direction$Leading$const = new Leading; - -class Trailing extends CustomType { -} -var Direction$Trailing$const = new Trailing; -function split2(x, substring) { - if (substring === "") { - return graphemes(x); - } else { - let _pipe = x; - let _pipe$1 = identity(_pipe); - let _pipe$2 = split(_pipe$1, substring); - return map2(_pipe$2, identity); - } -} -function concat_loop(loop$strings, loop$accumulator) { - while (true) { - let strings = loop$strings; - let accumulator = loop$accumulator; - if (strings instanceof Empty) { - return accumulator; - } else { - let string2 = strings.head; - let strings$1 = strings.tail; - loop$strings = strings$1; - loop$accumulator = accumulator + string2; - } - } -} -function concat2(strings) { - return concat_loop(strings, ""); -} - // build/dev/javascript/gleam_stdlib/gleam/dynamic/decode.mjs class DecodeError extends CustomType { constructor(expected, found, path) { @@ -1747,8 +1749,8 @@ class Inspector { new_str += '"'; return new_str; } - #utfCodepoint(codepoint2) { - return `//utfcodepoint(${String.fromCodePoint(codepoint2.value)})`; + #utfCodepoint(codepoint) { + return `//utfcodepoint(${String.fromCodePoint(codepoint.value)})`; } #bit_array(bits2) { if (bits2.bitSize === 0) { @@ -1847,6 +1849,47 @@ function isResult(data) { return Result$isOk(data) || Result$isError(data); } +// build/dev/javascript/gleam_stdlib/gleam/string_tree.mjs +class All extends CustomType { +} +var Direction$All$const = new All; + +// build/dev/javascript/gleam_stdlib/gleam/string.mjs +class Leading extends CustomType { +} +var Direction$Leading$const = new Leading; + +class Trailing extends CustomType { +} +var Direction$Trailing$const = new Trailing; +function split2(x, substring) { + if (substring === "") { + return graphemes(x); + } else { + let _pipe = x; + let _pipe$1 = identity(_pipe); + let _pipe$2 = split(_pipe$1, substring); + return map2(_pipe$2, identity); + } +} +function concat_loop(loop$strings, loop$accumulator) { + while (true) { + let strings = loop$strings; + let accumulator = loop$accumulator; + if (strings instanceof Empty) { + return accumulator; + } else { + let string4 = strings.head; + let strings$1 = strings.tail; + loop$strings = strings$1; + loop$accumulator = accumulator + string4; + } + } +} +function concat2(strings) { + return concat_loop(strings, ""); +} + // build/dev/javascript/gleam_stdlib/gleam/result.mjs function map4(result, fun) { if (result instanceof Ok) { @@ -1892,122 +1935,54 @@ function values2(results) { return result; }); } -// build/dev/javascript/gleam_erlang/gleam/erlang/process.mjs -class Normal extends CustomType { +// build/dev/javascript/gleam_json/gleam_json_ffi.mjs +function json_to_string(json) { + return JSON.stringify(json); } -var ExitReason$Normal$const = new Normal; -class Killed extends CustomType { +function object(entries) { + return Object.fromEntries(entries); } -var ExitReason$Killed$const = new Killed; -class Anything extends CustomType { +function identity2(x) { + return x; } -var AnythingSelectorTag$Anything$const = new Anything; - -class Process extends CustomType { +function decode(string4) { + try { + const result = JSON.parse(string4); + return Result$Ok(result); + } catch (err) { + return Result$Error(getJsonDecodeError(err, string4)); + } } -var ProcessMonitorFlag$Process$const = new Process; -class TimerNotFound extends CustomType { +function getJsonDecodeError(stdErr, json) { + if (isUnexpectedEndOfInput(stdErr)) + return DecodeError$UnexpectedEndOfInput(); + return toUnexpectedByteError(stdErr, json); } -var Cancelled$TimerNotFound$const = new TimerNotFound; -class Kill extends CustomType { +function isUnexpectedEndOfInput(err) { + const unexpectedEndOfInputRegex = /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i; + return unexpectedEndOfInputRegex.test(err.message); } -var KillFlag$Kill$const = new Kill; -// build/dev/javascript/gleam_otp/gleam/otp/system.mjs -class Running extends CustomType { +function toUnexpectedByteError(err, json) { + let converters = [ + v8UnexpectedByteError, + oldV8UnexpectedByteError, + jsCoreUnexpectedByteError, + spidermonkeyUnexpectedByteError + ]; + for (let converter of converters) { + let result = converter(err, json); + if (result) + return result; + } + return DecodeError$UnexpectedByte(""); } -var Mode$Running$const = new Running; -class Suspended extends CustomType { -} -var Mode$Suspended$const = new Suspended; -class NoDebug extends CustomType { -} -var DebugOption$NoDebug$const = new NoDebug; - -// build/dev/javascript/gleam_otp/gleam/otp/actor.mjs -class InitTimeout extends CustomType { -} -var StartError$InitTimeout$const = new InitTimeout; - -// build/dev/javascript/gleam_otp/gleam/otp/supervision.mjs -class Permanent extends CustomType { -} -var Restart$Permanent$const = new Permanent; -class Transient extends CustomType { -} -var Restart$Transient$const = new Transient; -class Temporary extends CustomType { -} -var Restart$Temporary$const = new Temporary; -class Supervisor extends CustomType { -} -var ChildType$Supervisor$const = new Supervisor; - -// build/dev/javascript/gleam_otp/gleam/otp/factory_supervisor.mjs -class SimpleOneForOne extends CustomType { -} -var Strategy$SimpleOneForOne$const = new SimpleOneForOne; - -// build/dev/javascript/gleam_stdlib/gleam/bool.mjs -function guard(requirement, consequence, alternative) { - if (requirement) { - return consequence; - } else { - return alternative(); - } -} - -// build/dev/javascript/gleam_stdlib/gleam/function.mjs -function identity2(x) { - return x; -} -// build/dev/javascript/gleam_json/gleam_json_ffi.mjs -function json_to_string(json) { - return JSON.stringify(json); -} -function object(entries) { - return Object.fromEntries(entries); -} -function identity3(x) { - return x; -} -function decode(string4) { - try { - const result = JSON.parse(string4); - return Result$Ok(result); - } catch (err) { - return Result$Error(getJsonDecodeError(err, string4)); - } -} -function getJsonDecodeError(stdErr, json) { - if (isUnexpectedEndOfInput(stdErr)) - return DecodeError$UnexpectedEndOfInput(); - return toUnexpectedByteError(stdErr, json); -} -function isUnexpectedEndOfInput(err) { - const unexpectedEndOfInputRegex = /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i; - return unexpectedEndOfInputRegex.test(err.message); -} -function toUnexpectedByteError(err, json) { - let converters = [ - v8UnexpectedByteError, - oldV8UnexpectedByteError, - jsCoreUnexpectedByteError, - spidermonkeyUnexpectedByteError - ]; - for (let converter of converters) { - let result = converter(err, json); - if (result) - return result; - } - return DecodeError$UnexpectedByte(""); -} -function v8UnexpectedByteError(err) { - const regex = /unexpected token '(.)', ".+" is not valid JSON/i; - const match = regex.exec(err.message); - if (!match) - return null; - const byte = toHex(match[1]); - return DecodeError$UnexpectedByte(byte); +function v8UnexpectedByteError(err) { + const regex = /unexpected token '(.)', ".+" is not valid JSON/i; + const match = regex.exec(err.message); + if (!match) + return null; + const byte = toHex(match[1]); + return DecodeError$UnexpectedByte(byte); } function oldV8UnexpectedByteError(err) { const regex = /unexpected token (.) in JSON at position (\d+)/i; @@ -2090,11 +2065,79 @@ function to_string2(json) { return json_to_string(json); } function string4(input) { - return identity3(input); + return identity2(input); } function object2(entries) { return object(entries); } +// build/dev/javascript/gleam_erlang/gleam/erlang/process.mjs +class Normal extends CustomType { +} +var ExitReason$Normal$const = new Normal; +class Killed extends CustomType { +} +var ExitReason$Killed$const = new Killed; +class Anything extends CustomType { +} +var AnythingSelectorTag$Anything$const = new Anything; + +class Process extends CustomType { +} +var ProcessMonitorFlag$Process$const = new Process; +class TimerNotFound extends CustomType { +} +var Cancelled$TimerNotFound$const = new TimerNotFound; +class Kill extends CustomType { +} +var KillFlag$Kill$const = new Kill; +// build/dev/javascript/gleam_otp/gleam/otp/system.mjs +class Running extends CustomType { +} +var Mode$Running$const = new Running; +class Suspended extends CustomType { +} +var Mode$Suspended$const = new Suspended; +class NoDebug extends CustomType { +} +var DebugOption$NoDebug$const = new NoDebug; + +// build/dev/javascript/gleam_otp/gleam/otp/actor.mjs +class InitTimeout extends CustomType { +} +var StartError$InitTimeout$const = new InitTimeout; + +// build/dev/javascript/gleam_otp/gleam/otp/supervision.mjs +class Permanent extends CustomType { +} +var Restart$Permanent$const = new Permanent; +class Transient extends CustomType { +} +var Restart$Transient$const = new Transient; +class Temporary extends CustomType { +} +var Restart$Temporary$const = new Temporary; +class Supervisor extends CustomType { +} +var ChildType$Supervisor$const = new Supervisor; + +// build/dev/javascript/gleam_otp/gleam/otp/factory_supervisor.mjs +class SimpleOneForOne extends CustomType { +} +var Strategy$SimpleOneForOne$const = new SimpleOneForOne; + +// build/dev/javascript/gleam_stdlib/gleam/bool.mjs +function guard(requirement, consequence, alternative) { + if (requirement) { + return consequence; + } else { + return alternative(); + } +} + +// build/dev/javascript/gleam_stdlib/gleam/function.mjs +function identity3(x) { + return x; +} // build/dev/javascript/houdini/houdini.ffi.mjs function escape(string5) { return string5.replaceAll(/[><&"']/g, (replaced) => { @@ -2344,9 +2387,6 @@ function placeholder(text) { function type_(control_type) { return attribute2("type", control_type); } -function value(control_value) { - return attribute2("value", control_value); -} // build/dev/javascript/lustre/lustre/effect.mjs class Effect extends CustomType { @@ -2406,9 +2446,9 @@ function get_or_compute(map5, key, compute) { function has_key(map5, key) { return map5 && map5.has(key); } -function insert2(map5, key, value2) { +function insert2(map5, key, value) { map5 ??= new Map; - map5.set(key, value2); + map5.set(key, value); return map5; } function remove(map5, key) { @@ -2573,10 +2613,10 @@ function map5(element2, mapper) { if (element2 instanceof Map2) { let child_mapper = element2.mapper; return new Map2(map_kind, element2.key, (handler) => { - return identity2(mapper)(child_mapper(handler)); - }, identity2(element2.child)); + return identity3(mapper)(child_mapper(handler)); + }, identity3(element2.child)); } else { - return new Map2(map_kind, element2.key, identity2(mapper), identity2(element2)); + return new Map2(map_kind, element2.key, identity3(mapper), identity3(element2)); } } function memo(key, dependencies, view) { @@ -2637,8 +2677,8 @@ function unsafe_raw_html(namespace, tag, attributes, inner_html) { function memo2(dependencies, view) { return memo("", dependencies, view); } -function ref(value2) { - return identity2(value2); +function ref(value) { + return identity3(value); } function map6(element3, f) { return map5(element3, f); @@ -2663,6 +2703,9 @@ function h1(attrs, children) { function h2(attrs, children) { return element2("h2", attrs, children); } +function h3(attrs, children) { + return element2("h3", attrs, children); +} function main(attrs, children) { return element2("main", attrs, children); } @@ -2678,6 +2721,9 @@ function p(attrs, children) { function a(attrs, children) { return element2("a", attrs, children); } +function small(attrs, children) { + return element2("small", attrs, children); +} function span(attrs, children) { return element2("span", attrs, children); } @@ -2825,11 +2871,11 @@ class Emit extends CustomType { } } class Provide extends CustomType { - constructor(kind, key, value2) { + constructor(kind, key, value) { super(); this.kind = kind; this.key = key; - this.value = value2; + this.value = value; } } class Subscribe extends CustomType { @@ -2853,25 +2899,25 @@ class Batch extends CustomType { this.messages = messages; } } -var ServerMessage$isBatch = (value2) => value2 instanceof Batch; +var ServerMessage$isBatch = (value) => value instanceof Batch; class AttributeChanged extends CustomType { - constructor(kind, name2, value2) { + constructor(kind, name2, value) { super(); this.kind = kind; this.name = name2; - this.value = value2; + this.value = value; } } -var ServerMessage$isAttributeChanged = (value2) => value2 instanceof AttributeChanged; +var ServerMessage$isAttributeChanged = (value) => value instanceof AttributeChanged; class PropertyChanged extends CustomType { - constructor(kind, name2, value2) { + constructor(kind, name2, value) { super(); this.kind = kind; this.name = name2; - this.value = value2; + this.value = value; } } -var ServerMessage$isPropertyChanged = (value2) => value2 instanceof PropertyChanged; +var ServerMessage$isPropertyChanged = (value) => value instanceof PropertyChanged; class EventFired extends CustomType { constructor(kind, path, name2, event2) { super(); @@ -2881,16 +2927,16 @@ class EventFired extends CustomType { this.event = event2; } } -var ServerMessage$isEventFired = (value2) => value2 instanceof EventFired; +var ServerMessage$isEventFired = (value) => value instanceof EventFired; class ContextProvided extends CustomType { - constructor(kind, key, value2) { + constructor(kind, key, value) { super(); this.kind = kind; this.key = key; - this.value = value2; + this.value = value; } } -var ServerMessage$isContextProvided = (value2) => value2 instanceof ContextProvided; +var ServerMessage$isContextProvided = (value) => value instanceof ContextProvided; var mount_kind = 0; var reconcile_kind = 1; var emit_kind = 2; @@ -2906,8 +2952,8 @@ function reconcile(patch, memos) { function emit(name2, data) { return new Emit(emit_kind, name2, data); } -function provide(key, value2) { - return new Provide(provide_kind, key, value2); +function provide(key, value) { + return new Provide(provide_kind, key, value); } function subscribe(key) { return new Subscribe(subscribe_kind, key); @@ -3418,7 +3464,7 @@ function get_handler(loop$events, loop$path, loop$mapper) { if ($1) { let handler = get2(events2.handlers, key); return new Ok(map3(handler, (handler2) => { - return new Handler(handler2.prevent_default, handler2.stop_propagation, identity2(mapper)(handler2.message)); + return new Handler(handler2.prevent_default, handler2.stop_propagation, identity3(mapper)(handler2.message)); })); } else { return error_nil; @@ -3442,7 +3488,7 @@ function get_handler(loop$events, loop$path, loop$mapper) { } function decode2(cache, path, name2, event3) { let parts = split_subtree_path(path + separator_event + name2); - let $ = get_handler(cache.events, parts, identity2); + let $ = get_handler(cache.events, parts, identity3); if ($ instanceof Ok) { let handler = $[0]; let $1 = run(event3, handler); @@ -3483,21 +3529,21 @@ class ClientDispatchedMessage extends CustomType { this.message = message; } } -var Message$isClientDispatchedMessage = (value2) => value2 instanceof ClientDispatchedMessage; +var Message$isClientDispatchedMessage = (value) => value instanceof ClientDispatchedMessage; class ClientRegisteredCallback extends CustomType { constructor(callback) { super(); this.callback = callback; } } -var Message$isClientRegisteredCallback = (value2) => value2 instanceof ClientRegisteredCallback; +var Message$isClientRegisteredCallback = (value) => value instanceof ClientRegisteredCallback; class ClientDeregisteredCallback extends CustomType { constructor(callback) { super(); this.callback = callback; } } -var Message$isClientDeregisteredCallback = (value2) => value2 instanceof ClientDeregisteredCallback; +var Message$isClientDeregisteredCallback = (value) => value instanceof ClientDeregisteredCallback; class EffectDispatchedMessage extends CustomType { constructor(message) { super(); @@ -3505,7 +3551,7 @@ class EffectDispatchedMessage extends CustomType { } } var Message$EffectDispatchedMessage = (message) => new EffectDispatchedMessage(message); -var Message$isEffectDispatchedMessage = (value2) => value2 instanceof EffectDispatchedMessage; +var Message$isEffectDispatchedMessage = (value) => value instanceof EffectDispatchedMessage; class EffectEmitEvent extends CustomType { constructor(name2, data) { super(); @@ -3514,16 +3560,16 @@ class EffectEmitEvent extends CustomType { } } var Message$EffectEmitEvent = (name2, data) => new EffectEmitEvent(name2, data); -var Message$isEffectEmitEvent = (value2) => value2 instanceof EffectEmitEvent; +var Message$isEffectEmitEvent = (value) => value instanceof EffectEmitEvent; class EffectProvidedValue extends CustomType { - constructor(key, value2) { + constructor(key, value) { super(); this.key = key; - this.value = value2; + this.value = value; } } -var Message$EffectProvidedValue = (key, value2) => new EffectProvidedValue(key, value2); -var Message$isEffectProvidedValue = (value2) => value2 instanceof EffectProvidedValue; +var Message$EffectProvidedValue = (key, value) => new EffectProvidedValue(key, value); +var Message$isEffectProvidedValue = (value) => value instanceof EffectProvidedValue; class EffectRequestedContextSubscription extends CustomType { constructor(key, decoder) { super(); @@ -3532,7 +3578,7 @@ class EffectRequestedContextSubscription extends CustomType { } } var Message$EffectRequestedContextSubscription = (key, decoder) => new EffectRequestedContextSubscription(key, decoder); -var Message$isEffectRequestedContextSubscription = (value2) => value2 instanceof EffectRequestedContextSubscription; +var Message$isEffectRequestedContextSubscription = (value) => value instanceof EffectRequestedContextSubscription; class EffectRemovedContextSubscription extends CustomType { constructor(key) { super(); @@ -3540,11 +3586,11 @@ class EffectRemovedContextSubscription extends CustomType { } } var Message$EffectRemovedContextSubscription = (key) => new EffectRemovedContextSubscription(key); -var Message$isEffectRemovedContextSubscription = (value2) => value2 instanceof EffectRemovedContextSubscription; +var Message$isEffectRemovedContextSubscription = (value) => value instanceof EffectRemovedContextSubscription; class SystemRequestedShutdown extends CustomType { } var Message$SystemRequestedShutdown$const = new SystemRequestedShutdown; -var Message$isSystemRequestedShutdown = (value2) => value2 instanceof SystemRequestedShutdown; +var Message$isSystemRequestedShutdown = (value) => value instanceof SystemRequestedShutdown; // build/dev/javascript/lustre/lustre/runtime/app.mjs class App extends CustomType { @@ -4604,7 +4650,7 @@ var insertBefore = (parent, node, reference) => parent.insertBefore(node, refere var moveBefore = SUPPORTS_MOVE_BEFORE ? (parent, node, reference) => parent.moveBefore(node, reference) : insertBefore; var removeChild = (parent, child2) => parent.removeChild(child2); var getAttribute = (node, name2) => node.getAttribute(name2); -var setAttribute = (node, name2, value2) => node.setAttribute(name2, value2); +var setAttribute = (node, name2, value) => node.setAttribute(name2, value); var removeAttribute = (node, name2) => node.removeAttribute(name2); var addEventListener = (node, name2, handler, options) => node.addEventListener(name2, handler, options); var removeEventListener = (node, name2, handler) => node.removeEventListener(name2, handler); @@ -4893,14 +4939,14 @@ class Reconciler { const { kind, name: name2, - value: value2, + value, prevent_default: prevent, debounce: debounceDelay, throttle: throttleDelay } = attribute3; switch (kind) { case attribute_kind: { - const valueOrDefault = value2 ?? ""; + const valueOrDefault = value ?? ""; if (name2 === "virtual:defaultValue") { node.defaultValue = valueOrDefault; return; @@ -4918,7 +4964,7 @@ class Reconciler { break; } case property_kind: - node[name2] = value2; + node[name2] = value; break; case event_kind: { if (handlers.has(name2)) { @@ -5017,8 +5063,8 @@ var syncedBooleanAttribute = (name2) => { }; var syncedAttribute = (name2) => { return { - added(node, value2) { - node[name2] = value2; + added(node, value) { + node[name2] = value; } }; }; @@ -5221,16 +5267,16 @@ var virtualiseAttributes = (node) => { }; var INPUT_ELEMENTS = ["input", "select", "textarea"]; var virtualiseInputEvents = (tag, node) => { - const value2 = node.value; + const value = node.value; const checked = node.checked; if (tag === "input" && node.type === "checkbox" && !checked) return; if (tag === "input" && node.type === "radio" && !checked) return; - if (node.type !== "checkbox" && node.type !== "radio" && !value2) + if (node.type !== "checkbox" && node.type !== "radio" && !value) return; queueMicrotask(() => { - node.value = value2; + node.value = value; node.checked = checked; node.dispatchEvent(new Event("input", { bubbles: true })); node.dispatchEvent(new Event("change", { bubbles: true })); @@ -5309,22 +5355,22 @@ class Runtime { const target = this.root.host ?? this.root; target.dispatchEvent(new LustreEvent(event3, data)); } - provide(key, value2) { + provide(key, value) { if (!this.#contexts.has(key)) { - this.#contexts.set(key, { value: value2, subscribers: [] }); + this.#contexts.set(key, { value, subscribers: [] }); } else { const context = this.#contexts.get(key); - if (isEqual2(context.value, value2)) { + if (isEqual2(context.value, value)) { return; } - context.value = value2; + context.value = value; for (let i = context.subscribers.length - 1;i >= 0; i--) { const [subscriber, unsubscribe2] = context.subscribers[i]; if (!subscriber) { context.subscribers.splice(i, 1); continue; } - subscriber(value2, unsubscribe2); + subscriber(value, unsubscribe2); } } } @@ -5333,12 +5379,12 @@ class Runtime { return; this.#contextSubscriptions.get(key)?.(); const target = this.root.host ?? this.root; - target.dispatchEvent(new ContextRequestEvent(key, (value2, unsubscribe2) => { + target.dispatchEvent(new ContextRequestEvent(key, (value, unsubscribe2) => { const previousUnsubscribe = this.#contextSubscriptions.get(key); if (previousUnsubscribe !== unsubscribe2) { previousUnsubscribe?.(); } - const decoded = run(value2, decoder); + const decoded = run(value, decoder); this.#contextSubscriptions.set(key, unsubscribe2); if (Result$isOk(decoded)) { this.dispatch(Result$Ok$0(decoded), true); @@ -5376,7 +5422,7 @@ class Runtime { emit: (event3, data) => this.emit(event3, data), select: () => {}, root: () => this.root, - provide: (key, value2) => this.provide(key, value2), + provide: (key, value) => this.provide(key, value), subscribe: (key, decoder) => this.subscribe(key, decoder), unsubscribe: (key) => this.unsubscribe(key) }; @@ -5538,13 +5584,13 @@ class Runtime2 { const { name: name2, data } = message; this.broadcast(emit(name2, data)); } else if (Message$isEffectProvidedValue(message)) { - const { key, value: value2 } = message; + const { key, value } = message; const existing = get(this.#providers, key); - if (Result$isOk(existing) && isEqual2(Result$Ok$0(existing), value2)) { + if (Result$isOk(existing) && isEqual2(Result$Ok$0(existing), value)) { return; } - this.#providers = insert(this.#providers, key, value2); - this.broadcast(provide(key, value2)); + this.#providers = insert(this.#providers, key, value); + this.broadcast(provide(key, value)); } else if (Message$isEffectRequestedContextSubscription(message)) { const { key, decoder } = message; this.broadcast(subscribe(key)); @@ -5586,15 +5632,15 @@ class Runtime2 { this.#model = model; return this.#view(model); } else if (ServerMessage$isAttributeChanged(message)) { - const { name: name2, value: value2 } = message; - const result = this.#handle_attribute_change(name2, value2); + const { name: name2, value } = message; + const result = this.#handle_attribute_change(name2, value); if (!Result$isOk(result)) { return this.#vdom; } return this.#dispatch(Result$Ok$0(result)); } else if (ServerMessage$isPropertyChanged(message)) { - const { name: name2, value: value2 } = message; - const result = this.#handle_properties_change(name2, value2); + const { name: name2, value } = message; + const result = this.#handle_properties_change(name2, value); if (!Result$isOk(result)) { return this.#vdom; } @@ -5609,12 +5655,12 @@ class Runtime2 { const { message: message2 } = Result$Ok$0(result); return this.#dispatch(message2); } else if (ServerMessage$isContextProvided(message)) { - const { key, value: value2 } = message; + const { key, value } = message; let result = get(this.#config.contexts, key); if (!Result$isOk(result)) { return this.#vdom; } - result = run(value2, Result$Ok$0(result)); + result = run(value, Result$Ok$0(result)); if (!Result$isOk(result)) { return this.#vdom; } @@ -5627,19 +5673,19 @@ class Runtime2 { this.#model = model; return this.#view(this.#model); } - #handle_attribute_change(name2, value2) { + #handle_attribute_change(name2, value) { const result = get(this.#config.attributes, name2); if (!Result$isOk(result)) { return result; } - return Result$Ok$0(result)(value2); + return Result$Ok$0(result)(value); } - #handle_properties_change(name2, value2) { + #handle_properties_change(name2, value) { const result = get(this.#config.properties, name2); if (!Result$isOk(result)) { return result; } - return Result$Ok$0(result)(value2); + return Result$Ok$0(result)(value); } #handle_effect(effect) { const dispatch2 = (message) => this.send(Message$EffectDispatchedMessage(message)); @@ -5650,7 +5696,7 @@ class Runtime2 { const internals = () => { return; }; - const provide2 = (key, value2) => this.send(Message$EffectProvidedValue(key, value2)); + const provide2 = (key, value) => this.send(Message$EffectProvidedValue(key, value)); const subscribe2 = (key, decoder) => this.send(Message$EffectRequestedContextSubscription(key, decoder)); const unsubscribe2 = (key) => this.send(Message$EffectRemovedContextSubscription(key)); globalThis.queueMicrotask(() => { @@ -5702,16 +5748,16 @@ function on_click(message) { return on("click", success(message)); } function on_input(message) { - return on("input", subfield(toList(["target", "value"]), string3, (value2) => { - return success(message(value2)); + return on("input", subfield(toList(["target", "value"]), string3, (value) => { + return success(message(value)); })); } function formdata_decoder() { let string_value_decoder = field(0, string3, (key) => { return field(1, one_of(map3(string3, (var0) => { return new Ok(var0); - }), toList([success(error_nil)])), (value2) => { - let _pipe2 = value2; + }), toList([success(error_nil)])), (value) => { + let _pipe2 = value; let _pipe$12 = map4(_pipe2, (_capture) => { return new$5(key, _capture); }); @@ -5739,21 +5785,21 @@ class PromiseLayer { constructor(promise) { this.promise = promise; } - static wrap(value2) { - return value2 instanceof Promise ? new PromiseLayer(value2) : value2; + static wrap(value) { + return value instanceof Promise ? new PromiseLayer(value) : value; } - static unwrap(value2) { - return value2 instanceof PromiseLayer ? value2.promise : value2; + static unwrap(value) { + return value instanceof PromiseLayer ? value.promise : value; } } -function resolve(value2) { - return Promise.resolve(PromiseLayer.wrap(value2)); +function resolve(value) { + return Promise.resolve(PromiseLayer.wrap(value)); } function then_await(promise, fn) { - return promise.then((value2) => fn(PromiseLayer.unwrap(value2))); + return promise.then((value) => fn(PromiseLayer.unwrap(value))); } function map_promise(promise, fn) { - return promise.then((value2) => PromiseLayer.wrap(fn(PromiseLayer.unwrap(value2)))); + return promise.then((value) => PromiseLayer.wrap(fn(PromiseLayer.unwrap(value)))); } function rescue(promise, fn) { return promise.catch((error) => fn(error)); @@ -6553,8 +6599,8 @@ function from_uri(uri) { }); }); } -function set_header(request, key2, value3) { - let headers = key_set(request.headers, lowercase(key2), value3); +function set_header(request, key2, value2) { + let headers = key_set(request.headers, lowercase(key2), value2); return new Request(request.method, headers, request.body, request.scheme, request.host, request.port, request.path, request.query); } function set_body(req, body2) { @@ -6669,7 +6715,7 @@ var from_relative_url = (url_string) => { return new Ok(uri); }; var uri_from_url = (url) => { - const optional = (value3) => value3 ? new Some(value3) : new None; + const optional = (value2) => value2 ? new Some(value2) : new None; return new Uri(optional(url.protocol?.slice(0, -1)), new None, optional(url.hostname), optional(url.port && Number(url.port)), url.pathname, optional(url.search?.slice(1)), optional(url.hash?.slice(1))); }; @@ -6847,6 +6893,26 @@ function post(url, body2, handler) { return reject(err, handler); } } +function delete$2(url, body2, handler) { + let $ = to_uri2(url); + if ($ instanceof Ok) { + let uri = $[0]; + let $1 = from_uri(uri); + if ($1 instanceof Ok) { + let request = $1[0]; + let _pipe = request; + let _pipe$1 = set_method(_pipe, Method$Delete$const); + let _pipe$2 = set_header(_pipe$1, "content-type", "application/json"); + let _pipe$3 = set_body(_pipe$2, to_string2(body2)); + return send3(_pipe$3, handler); + } else { + return reject(new BadUrl(url), handler); + } + } else { + let err = $[0]; + return reject(err, handler); + } +} // build/dev/javascript/shared/shared.mjs class Project extends CustomType { constructor(id, name2, description, tasks) { @@ -6878,10 +6944,11 @@ class Dashboard extends CustomType { } } class Credentials extends CustomType { - constructor(username, password) { + constructor(username, password, invite_password) { super(); this.username = username; this.password = password; + this.invite_password = invite_password; } } class NewProject extends CustomType { @@ -6944,41 +7011,43 @@ function dashboard_decoder() { }); }); } -function credentials_json(value3) { +function credentials_json(value2) { return object2(toList([ - ["username", string4(value3.username)], - ["password", string4(value3.password)] + ["username", string4(value2.username)], + ["password", string4(value2.password)], + ["invite_password", string4(value2.invite_password)] ])); } -function new_project_json(value3) { +function new_project_json(value2) { return object2(toList([ - ["name", string4(value3.name)], - ["description", string4(value3.description)] + ["name", string4(value2.name)], + ["description", string4(value2.description)] ])); } -function new_task_json(value3) { +function new_task_json(value2) { return object2(toList([ - ["title", string4(value3.title)], - ["description", string4(value3.description)], - ["assignee", string4(value3.assignee)], - ["assignees", string4(value3.assignees)], - ["due_date", string4(value3.due_date)] + ["title", string4(value2.title)], + ["description", string4(value2.description)], + ["assignee", string4(value2.assignee)], + ["assignees", string4(value2.assignees)], + ["due_date", string4(value2.due_date)] ])); } -function status_change_json(value3) { - return object2(toList([["status", string4(value3.status)]])); +function status_change_json(value2) { + return object2(toList([["status", string4(value2.status)]])); } // build/dev/javascript/client/client.mjs var FILEPATH = "src/client.gleam"; class Model extends CustomType { - constructor(page, dashboard, error, username, password, project_name, project_description, task_title, task_description, task_assignee, task_assignees, task_due_date) { + constructor(page, dashboard, error, username, password, invite_password, project_name, project_description, task_title, task_description, task_assignee, task_assignees, task_due_date) { super(); this.page = page; this.dashboard = dashboard; this.error = error; this.username = username; this.password = password; + this.invite_password = invite_password; this.project_name = project_name; this.project_description = project_description; this.task_title = task_title; @@ -7093,6 +7162,24 @@ class TaskDueDateChanged extends CustomType { this[0] = $0; } } +class InvitePasswordChanged extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class DeleteTask extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class DeleteProject extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} class LogOut extends CustomType { } var Message$LogOut$const = new LogOut; @@ -7102,47 +7189,106 @@ var Message$Nothing$const = new Nothing; function text_input(name2, placeholder2) { return input(toList([name(name2), placeholder(placeholder2)])); } -function task_view(task) { - let _block; +function status_button(task_id, status, label) { + return button(toList([ + class$("move-task"), + on_click(new StatusSubmitted(task_id, toList([["status", status]]))) + ]), toList([text3(label)])); +} +function right_status_button(task) { let $ = task.status; if ($ === "todo") { - _block = "doing"; + return status_button(task.id, "doing", "→"); } else if ($ === "doing") { - _block = "done"; + return status_button(task.id, "done", "→"); + } else { + return none2(); + } +} +function left_status_button(task) { + let $ = task.status; + if ($ === "doing") { + return status_button(task.id, "todo", "←"); + } else if ($ === "done") { + return status_button(task.id, "doing", "←"); + } else { + return none2(); + } +} +function if_blank(value2, fallback) { + let $ = value2 === ""; + if ($) { + return fallback; } else { - _block = "todo"; + return value2; } - let next = _block; +} +function task_view(task) { return div(toList([class$("task")]), toList([ - div(List$Empty$const, toList([ - strong(List$Empty$const, toList([text3(task.title)])), - p(List$Empty$const, toList([text3(task.description)])) + div(toList([class$("task-header")]), toList([ + strong(toList([class$("task-title")]), toList([text3(task.title)])), + button(toList([ + on_click(new DeleteTask(task.id)), + class$("delete-task") + ]), toList([text3("Delete")])) ])), - div(toList([class$("task-meta")]), toList([ - span(List$Empty$const, toList([text3(task.status)])), - text3("Owner: " + task.assignee), - text3("Assignees: " + task.assignees), - form(toList([ - on_submit((fields) => { - return new StatusSubmitted(task.id, fields); - }) - ]), toList([ - input(toList([ - type_("hidden"), - name("status"), - value(next) - ])), - button(List$Empty$const, toList([text3("Move to " + next)])) + p(toList([class$("task-description")]), toList([text3(task.description)])), + div(toList([class$("task-details")]), toList([ + div(toList([class$("task-person")]), toList([ + small(List$Empty$const, toList([text3("Owner")])), + text3(if_blank(task.assignee, "Unassigned")) + ])), + div(toList([class$("task-person")]), toList([ + small(List$Empty$const, toList([text3("Assignees")])), + text3(if_blank(task.assignees, "None")) + ])), + div(toList([class$("task-due")]), toList([ + small(List$Empty$const, toList([text3("Due")])), + text3((() => { + let $ = task.due_date; + if ($ === "") { + return "No due date"; + } else { + return $; + } + })()) ])) + ])), + div(toList([class$("task-footer")]), toList([ + span(toList([class$("task-status")]), toList([text3(task.status)])), + div(toList([class$("task-actions")]), toList([left_status_button(task), right_status_button(task)])) ])) ])); } +function kanban_column(title2, tasks) { + return section(toList([class$("kanban-column")]), append(toList([ + h3(List$Empty$const, toList([ + text3(title2 + " · " + to_string(length(tasks))) + ])) + ]), map2(tasks, task_view))); +} function project_view(_, project) { + let backlog = filter(project.tasks, (task) => { + return task.status === "todo"; + }); + let doing = filter(project.tasks, (task) => { + return task.status === "doing"; + }); + let done = filter(project.tasks, (task) => { + return task.status === "done"; + }); return article(toList([class$("panel")]), append(toList([ text3("PROJECT"), h2(List$Empty$const, toList([text3(project.name)])), - p(List$Empty$const, toList([text3(project.description)])) - ]), append(map2(project.tasks, task_view), toList([ + p(List$Empty$const, toList([text3(project.description)])), + button(toList([on_click(new DeleteProject(project.id))]), toList([text3("Delete project")])) + ]), append(toList([ + div(toList([class$("kanban")]), toList([ + kanban_column("To do", backlog), + kanban_column("In progress", doing), + kanban_column("Done", done) + ])) + ]), toList([ form(toList([ on_submit((fields) => { return new TaskSubmitted(project.id, fields); @@ -7151,14 +7297,14 @@ function project_view(_, project) { text_input("title", "Task title"), text_input("assignee", "Owner"), text_input("assignees", "Assignees"), - text_input("due_date", "Due date"), + text_input("due_date", "Due (MM/DD/YY)"), text_input("description", "Details"), button(List$Empty$const, toList([text3("Add task")])) ])) ])))); } function styles() { - return style(List$Empty$const, "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;border-radius:22px;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:#b53b24;font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:#b53b24;font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;border-radius:22px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;border-radius:18px;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.task{display:flex;justify-content:space-between;gap:20px;padding:17px 0;border-top:1px solid #eee8df}.task p{margin:5px 0 0;color:#796f64}.task-meta{display:flex;align-items:center;justify-content:flex-end;gap:9px;flex-wrap:wrap;color:#796f64;font-size:.82rem}.task-meta span{padding:5px 9px;border-radius:99px;background:#f0ebe3}.task-meta button{padding:7px 10px;font-size:.78rem}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input{min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;border-radius:10px;background:#fffcf8;color:#28231e;font:inherit}button{border:0;border-radius:10px;background:#b53b24;color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:#b53b24;font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:650px){.task{display:block}.task-meta{justify-content:flex-start;margin-top:12px}header{display:block}.hero{padding:28px}}"); + return style(List$Empty$const, "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:#b53b24;font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:#b53b24;font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.kanban{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:22px}.kanban-column{background:#f7f3ed;border:1px solid #ebe1d4;padding:13px}.kanban-column h3{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#796f64;margin:0 0 5px}.task{display:flex;flex-direction:column;gap:13px;background:#fff;border:1px solid #e8dfd4;padding:16px;margin:9px 0;box-shadow:0 4px 12px #2d24140a;min-width:0}.task-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-title{font-size:1rem;line-height:1.25;overflow-wrap:anywhere}.delete-task{flex:0 0 auto;padding:5px 8px;font-size:.68rem;background:#f0ebe3;color:#796f64}.delete-task:hover{background:#e8dfd4;color:#28231e}.task-description{margin:0;color:#796f64;line-height:1.45;overflow-wrap:anywhere}.task-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px 14px;padding-top:12px;border-top:1px solid #eee6dc;color:#4e463e;font-size:.8rem}.task-person,.task-due{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-wrap:anywhere}.task-details small{color:#a04a35;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-due{grid-column:1/-1}.task-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}.task-status{padding:5px 8px;background:#f0ebe3;color:#796f64;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-actions{display:flex;gap:6px;margin-left:auto}.task-actions form{margin:0}.move-task{width:31px;height:29px;padding:0;font-size:1.05rem;line-height:1}.move-task:hover{background:#902e1d}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input{min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;background:#fffcf8;color:#28231e;font:inherit}button{border:0;background:#b53b24;color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:#b53b24;font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:800px){.kanban{grid-template-columns:1fr 1fr}}@media(max-width:650px){.kanban{grid-template-columns:1fr}.task-details{grid-template-columns:1fr}.task-due{grid-column:auto}header{display:block}.hero{padding:28px}}"); } function error_view(error) { if (error instanceof Some) { @@ -7177,7 +7323,7 @@ function dashboard_view(model) { header(List$Empty$const, toList([ div(List$Empty$const, toList([ text3("FRC 972 · IRON CLAW"), - h1(List$Empty$const, toList([text3("Team board")])) + h1(List$Empty$const, toList([text3("project board")])) ])), button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) ])), @@ -7190,7 +7336,7 @@ function dashboard_view(model) { }) ]), toList([ text_input("name", "Project name"), - text_input("description", "What does success look like?"), + text_input("description", "Description/Goals"), button(List$Empty$const, toList([text3("Add project")])) ])) ])) @@ -7204,7 +7350,7 @@ function dashboard_view(model) { ])); } } -function auth_view(model, title2, submit, on_submit2) { +function creat_account_view(model, title2, submit, on_submit2) { let _block; let $ = submit === "Log in"; if ($) { @@ -7227,6 +7373,58 @@ function auth_view(model, title2, submit, on_submit2) { h1(List$Empty$const, toList([text3(title2)])), p(List$Empty$const, toList([text3("google sheets, more like shmoogle shits")])), error_view(model.error), + form(toList([on_submit(on_submit2)]), toList([ + input(toList([ + name("username"), + placeholder("Username"), + on_input((var0) => { + return new UsernameChanged(var0); + }) + ])), + input(toList([ + name("invite_password"), + type_("password"), + placeholder("Team signup password"), + on_input((var0) => { + return new InvitePasswordChanged(var0); + }) + ])), + input(toList([ + name("password"), + type_("password"), + placeholder("Password"), + on_input((var0) => { + return new PasswordChanged(var0); + }) + ])), + button(List$Empty$const, toList([text3(submit)])) + ])), + a(toList([href(link)]), toList([text3(link_text)])) + ])); +} +function auth_view(model, title2, submit, on_submit2) { + let _block; + let $ = submit === "Log in"; + if ($) { + _block = "/register"; + } else { + _block = "/login"; + } + let link = _block; + let _block$1; + let $1 = submit === "Log in"; + if ($1) { + _block$1 = "Need an account?"; + } else { + _block$1 = "Already registered?"; + } + let link_text = _block$1; + return main(toList([class$("auth")]), toList([ + styles(), + text3("Team 972 · IRON CLAW"), + h1(List$Empty$const, toList([text3(title2)])), + p(List$Empty$const, toList([text3("google sheets, more like shmoogle shits")])), + error_view(model.error), form(toList([on_submit(on_submit2)]), toList([ input(toList([ name("username"), @@ -7255,7 +7453,7 @@ function view(model) { return new LoginSubmitted(var0); }); } else if ($ === "register") { - return auth_view(model, "Create your account", "Create account", (var0) => { + return creat_account_view(model, "Create your account", "Create account", (var0) => { return new RegisterSubmitted(var0); }); } else { @@ -7266,9 +7464,12 @@ function field2(fields, name2) { let _pipe = key_find(fields, name2); return unwrap2(_pipe, ""); } +function if_empty_invite(fields) { + return field2(fields, "invite_password"); +} function auth_request(model, fields, url, _) { let _block; - let _pipe = new Credentials(field2(fields, "username"), field2(fields, "password")); + let _pipe = new Credentials(field2(fields, "username"), field2(fields, "password"), if_empty_invite(fields)); _block = credentials_json(_pipe); let body2 = _block; return [ @@ -7287,16 +7488,14 @@ function update2(model, message2) { if (message2 instanceof DashboardLoaded) { let $ = message2[0]; if ($ instanceof Ok) { - let value3 = $[0]; - echo("dashload good", undefined, "src/client.gleam", 103); + let value2 = $[0]; return [ - new Model(model.page, new Some(value3), Option$None$const, model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("dashboard", new Some(value2), Option$None$const, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else { - echo("dashload err", undefined, "src/client.gleam", 107); return [ - new Model("login", model.dashboard, new Some("Please sign in to continue."), model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("login", model.dashboard, new Some("Please sign in to continue."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } @@ -7364,14 +7563,14 @@ function update2(model, message2) { let $ = message2[0]; if ($ instanceof Ok) { return [ - new Model("dashboard", model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("dashboard", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { return new DashboardLoaded(var0); })) ]; } else { return [ - new Model(model.page, model.dashboard, new Some("Invalid username or password."), model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, new Some("Invalid username or password."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } @@ -7379,74 +7578,106 @@ function update2(model, message2) { let $ = message2[0]; if ($ instanceof Ok) { return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, "", "", "", "", "", "", ""), + new Model(model.page, model.dashboard, model.error, model.username, model.password, "", "", "", "", "", "", "", ""), get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { return new DashboardLoaded(var0); })) ]; } else { return [ - new Model(model.page, model.dashboard, new Some("The request could not be saved."), model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, new Some("The request could not be saved."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } } else if (message2 instanceof UsernameChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, value3, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, value2, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof PasswordChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, value3, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, value2, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof ProjectNameChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, value3, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, value2, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof ProjectDescriptionChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.project_name, value3, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, value2, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof TaskTitleChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, value3, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, value2, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof TaskDescriptionChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, model.task_title, value3, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, value2, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof TaskAssigneeChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, value3, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, value2, model.task_assignees, model.task_due_date), none() ]; } else if (message2 instanceof TaskAssigneesChanged) { - let value3 = message2[0]; + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, value3, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, value2, model.task_due_date), none() ]; } else if (message2 instanceof TaskDueDateChanged) { - let value3 = message2[0]; + let value2 = message2[0]; + return [ + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, value2), + none() + ]; + } else if (message2 instanceof InvitePasswordChanged) { + let value2 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, value3), + new Model(model.page, model.dashboard, model.error, model.username, model.password, value2, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), none() ]; + } else if (message2 instanceof DeleteTask) { + let task_id = message2[0]; + return [ + model, + delete$2("/api/tasks/" + to_string(task_id), object2(List$Empty$const), expect_any_response((result) => { + return new Saved((() => { + let _pipe = result; + return map4(_pipe, (response) => { + return response.body; + }); + })()); + })) + ]; + } else if (message2 instanceof DeleteProject) { + let project_id = message2[0]; + return [ + model, + delete$2("/api/projects/" + to_string(project_id), object2(List$Empty$const), expect_any_response((result) => { + return new Saved((() => { + let _pipe = result; + return map4(_pipe, (response) => { + return response.body; + }); + })()); + })) + ]; } else if (message2 instanceof LogOut) { return [ - new Model("login", model.dashboard, model.error, model.username, model.password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("login", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), get3("/api/logout", expect_ok_response((_) => { return Message$Nothing$const; })) @@ -7471,238 +7702,33 @@ function init(_) { _block = "dashboard"; } let page = _block; - echo(page, undefined, "src/client.gleam", 68); - let model = new Model(page, Option$None$const, Option$None$const, "", "", "", "", "", "", "", "", ""); - return auth_request(model, List$Empty$const, "/api/login", "/"); + let model = new Model(page, Option$None$const, Option$None$const, "", "", "", "", "", "", "", "", "", ""); + if (page === "dashboard") { + return [ + model, + get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { + return new DashboardLoaded(var0); + })) + ]; + } else if (page === "login") { + return [ + model, + get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { + return new DashboardLoaded(var0); + })) + ]; + } else { + return [model, none()]; + } } function main2() { let app = application(init, update2, view); let $ = start4(app, "#app", Option$None$const); if (!($ instanceof Ok)) { - throw makeError("let_assert", FILEPATH, "client", 18, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 421, end: 471, pattern_start: 432, pattern_end: 437 }); + throw makeError("let_assert", FILEPATH, "client", 19, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 439, end: 489, pattern_start: 450, pattern_end: 455 }); } return; } -function echo(value3, message2, file, line) { - const grey = "\x1B[90m"; - const reset_color = "\x1B[39m"; - const file_line = `${file}:${line}`; - const inspector = new Echo$Inspector; - const string_value = inspector.inspect(value3); - const string_message = message2 === undefined ? "" : " " + message2; - if (globalThis.process?.stderr?.write) { - const string6 = `${grey}${file_line}${reset_color}${string_message} -${string_value} -`; - globalThis.process.stderr.write(string6); - } else if (globalThis.Deno) { - const string6 = `${grey}${file_line}${reset_color}${string_message} -${string_value} -`; - globalThis.Deno.stderr.writeSync(new TextEncoder().encode(string6)); - } else { - const string6 = `${file_line}${string_message} -${string_value}`; - globalThis.console.log(string6); - } - return value3; -} - -class Echo$Inspector { - #references = new globalThis.Set; - #isDict(value3) { - try { - const empty_dict = make(); - const dict_class = empty_dict.constructor; - return value3 instanceof dict_class; - } catch { - return false; - } - } - #float(float3) { - const string6 = float3.toString().replace("+", ""); - if (string6.indexOf(".") >= 0) { - return string6; - } else { - const index5 = string6.indexOf("e"); - if (index5 >= 0) { - return string6.slice(0, index5) + ".0" + string6.slice(index5); - } else { - return string6 + ".0"; - } - } - } - inspect(v) { - const t = typeof v; - if (v === true) - return "True"; - if (v === false) - return "False"; - if (v === null) - return "//js(null)"; - if (v === undefined) - return "Nil"; - if (t === "string") - return this.#string(v); - if (t === "bigint" || globalThis.Number.isInteger(v)) - return v.toString(); - if (t === "number") - return this.#float(v); - if (v instanceof UtfCodepoint) - return this.#utfCodepoint(v); - if (v instanceof BitArray) - return this.#bit_array(v); - if (v instanceof globalThis.RegExp) - return `//js(${v})`; - if (v instanceof globalThis.Date) - return `//js(Date("${v.toISOString()}"))`; - if (v instanceof globalThis.Error) - return `//js(${v.toString()})`; - if (v instanceof globalThis.Function) { - const args = []; - for (const i of globalThis.Array(v.length).keys()) - args.push(globalThis.String.fromCharCode(i + 97)); - return `//fn(${args.join(", ")}) { ... }`; - } - if (this.#references.size === this.#references.add(v).size) { - return "//js(circular reference)"; - } - let printed; - if (globalThis.Array.isArray(v)) { - printed = `#(${v.map((v2) => this.inspect(v2)).join(", ")})`; - } else if (v instanceof List) { - printed = this.#list(v); - } else if (v instanceof CustomType) { - printed = this.#customType(v); - } else if (this.#isDict(v)) { - printed = this.#dict(v); - } else if (v instanceof Set) { - return `//js(Set(${[...v].map((v2) => this.inspect(v2)).join(", ")}))`; - } else { - printed = this.#object(v); - } - this.#references.delete(v); - return printed; - } - #object(v) { - const name2 = globalThis.Object.getPrototypeOf(v)?.constructor?.name || "Object"; - const props = []; - for (const k of globalThis.Object.keys(v)) { - props.push(`${this.inspect(k)}: ${this.inspect(v[k])}`); - } - const body2 = props.length ? " " + props.join(", ") + " " : ""; - const head = name2 === "Object" ? "" : name2 + " "; - return `//js(${head}{${body2}})`; - } - #dict(map10) { - let body2 = "dict.from_list(["; - let first = true; - let key_value_pairs = fold(map10, [], (pairs, key2, value3) => { - pairs.push([key2, value3]); - return pairs; - }); - key_value_pairs.sort(); - key_value_pairs.forEach(([key2, value3]) => { - if (!first) - body2 = body2 + ", "; - body2 = body2 + "#(" + this.inspect(key2) + ", " + this.inspect(value3) + ")"; - first = false; - }); - return body2 + "])"; - } - #customType(record) { - const props = globalThis.Object.keys(record).map((label) => { - const value3 = this.inspect(record[label]); - return isNaN(parseInt(label)) ? `${label}: ${value3}` : value3; - }).join(", "); - return props ? `${record.constructor.name}(${props})` : record.constructor.name; - } - #list(list4) { - if (list4 instanceof Empty) { - return "[]"; - } - let char_out = 'charlist.from_string("'; - let list_out = "["; - let current = list4; - while (current instanceof NonEmpty) { - let element4 = current.head; - current = current.tail; - if (list_out !== "[") { - list_out += ", "; - } - list_out += this.inspect(element4); - if (char_out) { - if (globalThis.Number.isInteger(element4) && element4 >= 32 && element4 <= 126) { - char_out += globalThis.String.fromCharCode(element4); - } else { - char_out = null; - } - } - } - if (char_out) { - return char_out + '")'; - } else { - return list_out + "]"; - } - } - #string(str) { - let new_str = '"'; - for (let i = 0;i < str.length; i++) { - const char = str[i]; - switch (char) { - case ` -`: - new_str += "\\n"; - break; - case "\r": - new_str += "\\r"; - break; - case "\t": - new_str += "\\t"; - break; - case "\f": - new_str += "\\f"; - break; - case "\\": - new_str += "\\\\"; - break; - case '"': - new_str += "\\\""; - break; - default: - if (char < " " || char > "~" && char < " ") { - new_str += "\\u{" + char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0") + "}"; - } else { - new_str += char; - } - } - } - new_str += '"'; - return new_str; - } - #utfCodepoint(codepoint2) { - return `//utfcodepoint(${globalThis.String.fromCodePoint(codepoint2.value)})`; - } - #bit_array(bits2) { - if (bits2.bitSize === 0) { - return "<<>>"; - } - let acc = "<<"; - for (let i = 0;i < bits2.byteSize - 1; i++) { - acc += bits2.byteAt(i).toString(); - acc += ", "; - } - if (bits2.byteSize * 8 === bits2.bitSize) { - acc += bits2.byteAt(bits2.byteSize - 1).toString(); - } else { - const trailingBitsCount = bits2.bitSize % 8; - acc += bits2.byteAt(bits2.byteSize - 1) >> 8 - trailingBitsCount; - acc += `:size(${trailingBitsCount})`; - } - acc += ">>"; - return acc; - } -} // .lustre/build/client.mjs main2(); diff --git a/shared/src/shared.gleam b/shared/src/shared.gleam index 7aaf0fd..96e4d6e 100644 --- a/shared/src/shared.gleam +++ b/shared/src/shared.gleam @@ -23,7 +23,7 @@ pub type Dashboard { } pub type Credentials { - Credentials(username: String, password: String) + Credentials(username: String, password: String, invite_password: String) } pub type NewProject { @@ -73,7 +73,8 @@ fn task_decoder() -> decode.Decoder(Task) { pub fn credentials_decoder() -> decode.Decoder(Credentials) { use username <- decode.field("username", decode.string) use password <- decode.field("password", decode.string) - decode.success(Credentials(username:, password:)) + use invite_password <- decode.field("invite_password", decode.string) + decode.success(Credentials(username:, password:, invite_password:)) } pub fn new_project_decoder() -> decode.Decoder(NewProject) { @@ -130,6 +131,7 @@ pub fn credentials_json(value: Credentials) -> json.Json { json.object([ #("username", json.string(value.username)), #("password", json.string(value.password)), + #("invite_password", json.string(value.invite_password)), ]) } -- 2.47.3