]> git.taranathan.com Git - robotics-task-manager.git/commitdiff
works
authorTaran Nathan <moogoesmeow123@gmail.com>
Thu, 24 Sep 2026 21:38:10 +0000 (14:38 -0700)
committerTaran Nathan <moogoesmeow123@gmail.com>
Thu, 24 Sep 2026 23:54:30 +0000 (16:54 -0700)
client/src/client.gleam
server/README.md
server/src/server.gleam
server/src/sql.gleam
server/static/client.js
shared/src/shared.gleam

index dc4f0a792ab12d60a9f1e9e135b744c67d6346fb..c08fdf310f44e62fd6b601d7c2e10ce4467e75c6 100644 (file)
@@ -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
 }
index abe38586117d2822c7d75d28781de033d3465031..036b22fdeb0f5e7bc08377ddf040e1c8149c4495 100644 (file)
@@ -25,7 +25,10 @@ gleam run -m lustre/dev build
 cp dist/client.js ../server/static/client.js
 ```
 
-Then open <http://localhost:3000/register> to create the first account.
+Then open <http://localhost:3000/register> 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
index f7452c1b772c75a9b69c87e76ba256eca239a1d0..aeb249a783ee4823d992a7acdb4eb075e7fa9179 100644 (file)
@@ -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) {
index 96d6464b8812640678c1479f8b2229735fd9dffd..165e4732f429f828c5ca4a49c4a1d6b166a87906 100644 (file)
@@ -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
index 537228d78352cb870f3db2d4810deca0bb843348..1e4474059c4de498c4dc32b4c4a6d002dbd16788 100644 (file)
@@ -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();
index 7aaf0fd7331184219512b1892eb83de0f65ef72d..96e4d6e4d64b8a9ab72261add56e30b8331a39f5 100644 (file)
@@ -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)),
   ])
 }