]> git.taranathan.com Git - robotics-task-manager.git/commitdiff
speed up with state modifications before psql response
authorTaran Nathan <moogoesmeow123@gmail.com>
Thu, 24 Sep 2026 23:54:44 +0000 (16:54 -0700)
committerTaran Nathan <moogoesmeow123@gmail.com>
Fri, 25 Sep 2026 00:23:54 +0000 (17:23 -0700)
client/src/client.gleam
server/static/client.js

index c08fdf310f44e62fd6b601d7c2e10ce4467e75c6..a47dc990c586cb99d0ebe8369a8caa6a008c866a 100644 (file)
@@ -159,7 +159,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
         shared.NewProject(field(fields, "name"), field(fields, "description"))
         |> shared.new_project_json
       #(
-        model,
+        Model(..model, project_name: "", project_description: ""),
         rsvp.post(
           "/api/projects",
           body,
@@ -180,7 +180,14 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
         )
         |> shared.new_task_json
       #(
-        model,
+        Model(
+          ..model,
+          task_title: "",
+          task_description: "",
+          task_assignee: "",
+          task_assignees: "",
+          task_due_date: "",
+        ),
         rsvp.post(
           "/api/projects/" <> int.to_string(project_id) <> "/tasks",
           body,
@@ -194,7 +201,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
       let status = field(fields, "status")
       let body = shared.StatusChange(status) |> shared.status_change_json
       #(
-        model,
+        update_task_status(model, task_id, status),
         rsvp.post(
           "/api/tasks/" <> int.to_string(task_id) <> "/status",
           body,
@@ -205,7 +212,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
       )
     }
     DeleteTask(task_id) -> #(
-      model,
+      remove_task(model, task_id),
       rsvp.delete(
         "/api/tasks/" <> int.to_string(task_id),
         json.object([]),
@@ -215,7 +222,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
       ),
     )
     DeleteProject(project_id) -> #(
-      model,
+      remove_project(model, project_id),
       rsvp.delete(
         "/api/projects/" <> int.to_string(project_id),
         json.object([]),
@@ -264,6 +271,61 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
   }
 }
 
+fn update_task_status(model: Model, task_id: Int, status: String) -> Model {
+  case model.dashboard {
+    None -> model
+    Some(board) -> {
+      let projects =
+        list.map(board.projects, fn(project) {
+          let tasks =
+            list.map(project.tasks, fn(task) {
+              case task.id == task_id {
+                True -> shared.Task(..task, status: status)
+                False -> task
+              }
+            })
+          shared.Project(..project, tasks: tasks)
+        })
+      Model(
+        ..model,
+        dashboard: Some(shared.Dashboard(..board, projects: projects)),
+      )
+    }
+  }
+}
+
+fn remove_task(model: Model, task_id: Int) -> Model {
+  case model.dashboard {
+    None -> model
+    Some(board) -> {
+      let projects =
+        list.map(board.projects, fn(project) {
+          let tasks =
+            list.filter(project.tasks, fn(task) { task.id != task_id })
+          shared.Project(..project, tasks: tasks)
+        })
+      Model(
+        ..model,
+        dashboard: Some(shared.Dashboard(..board, projects: projects)),
+      )
+    }
+  }
+}
+
+fn remove_project(model: Model, project_id: Int) -> Model {
+  case model.dashboard {
+    None -> model
+    Some(board) -> {
+      let projects =
+        list.filter(board.projects, fn(project) { project.id != project_id })
+      Model(
+        ..model,
+        dashboard: Some(shared.Dashboard(..board, projects: projects)),
+      )
+    }
+  }
+}
+
 fn auth_request(
   model: Model,
   fields: List(#(String, String)),
@@ -291,11 +353,11 @@ fn auth_request(
 
 fn view(model: Model) -> Element(Message) {
   case model.page {
-    "login" -> auth_view(model, "Welcome back", "Log in", LoginSubmitted)
+    "login" -> auth_view(model, "Greetings Comrade", "Log in", LoginSubmitted)
     "register" ->
       creat_account_view(
         model,
-        "Create your account",
+        "Create account",
         "Create account",
         RegisterSubmitted,
       )
@@ -359,7 +421,11 @@ fn creat_account_view(
     styles(),
     html.text("FRC 972 · IRON CLAW"),
     html.h1([], [html.text(title)]),
-    html.p([], [html.text("google sheets, more like shmoogle shits")]),
+    html.p([], [
+      html.text(
+        "google sheets, more like shmoogle shits (also passwords need to be at least 8 chars and username needs to be at least 3)",
+      ),
+    ]),
     error_view(model.error),
     html.form([event.on_submit(on_submit)], [
       html.input([
@@ -389,7 +455,7 @@ fn dashboard_view(model: Model) -> Element(Message) {
   case model.dashboard {
     None ->
       html.main([attribute.class("auth")], [
-        html.h1([], [html.text("Loading Iron Claw...")]),
+        html.h1([], [html.text("Loading Iron Claw")]),
         error_view(model.error),
       ])
     Some(board) ->
@@ -450,14 +516,44 @@ fn project_view(_model: Model, project: shared.Project) -> Element(Message) {
         ],
         [
           html.form(
-            [event.on_submit(fn(fields) { TaskSubmitted(project.id, fields) })],
+            [
+              attribute.class("task-form"),
+              attribute.styles([
+                #("display", "grid"),
+                #("grid-template-columns", "repeat(4, minmax(0, 1fr))"),
+                #("align-items", "start"),
+              ]),
+              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")]),
+              html.textarea(
+                [
+                  attribute.class("task-description-input"),
+                  attribute.styles([
+                    #("grid-column", "1 / -1"),
+                    #("width", "100%"),
+                    #("box-sizing", "border-box"),
+                    #("min-height", "96px"),
+                    #("resize", "vertical"),
+                    #("padding", "12px 13px"),
+                    #("border", "1px solid #d9cfc2"),
+                    #("background", "#fffcf8"),
+                    #("color", "#28231e"),
+                    #("font", "inherit"),
+                  ]),
+                  attribute.name("description"),
+                  attribute.placeholder("Details"),
+                  attribute.rows(4),
+                ],
+                "",
+              ),
+              html.button([attribute.class("task-submit")], [
+                html.text("Add task"),
+              ]),
             ],
           ),
         ],
@@ -491,7 +587,13 @@ fn task_view(task: shared.Task) -> Element(Message) {
         ],
       ),
     ]),
-    html.p([attribute.class("task-description")], [html.text(task.description)]),
+    html.p(
+      [
+        attribute.class("task-description"),
+        attribute.style("white-space", "pre-wrap"),
+      ],
+      [html.text(task.description)],
+    ),
     html.div([attribute.class("task-details")], [
       html.div([attribute.class("task-person")], [
         html.small([], [html.text("Owner")]),
@@ -566,7 +668,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;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}}",
+    "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:rgb(255, 100, 0);font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:rgb(255, 100, 0);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:#000000;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:rgb(255, 100, 0);color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:rgb(255, 100, 0);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}}",
   )
 }
 
index 1e4474059c4de498c4dc32b4c4a6d002dbd16788..cfce598af054dbc81c53303525f85aedb899bcf5 100644 (file)
@@ -2239,6 +2239,9 @@ var always = /* @__PURE__ */ new Always(always_kind);
 function attribute(name, value) {
   return new Attribute(attribute_kind, name, value);
 }
+function property(name, value) {
+  return new Property(property_kind, name, value);
+}
 function event(name, handler, include, prevent_default, stop_propagation, debounce, throttle) {
   return new Event2(event_kind, name, handler, include, prevent_default, stop_propagation, debounce, throttle);
 }
@@ -2372,9 +2375,53 @@ function prepare(attributes) {
 function attribute2(name, value) {
   return attribute(name, value);
 }
+function property2(name, value) {
+  return property(name, value);
+}
 function class$(name) {
   return attribute2("class", name);
 }
+function style(property3, value) {
+  if (property3 === "") {
+    return class$("");
+  } else if (value === "") {
+    return class$("");
+  } else {
+    return attribute2("style", property3 + ":" + value + ";");
+  }
+}
+function do_styles(loop$properties, loop$styles) {
+  while (true) {
+    let properties = loop$properties;
+    let styles = loop$styles;
+    if (properties instanceof Empty) {
+      return styles;
+    } else {
+      let $ = properties.head[0];
+      if ($ === "") {
+        let rest = properties.tail;
+        loop$properties = rest;
+        loop$styles = styles;
+      } else {
+        let $1 = properties.head[1];
+        if ($1 === "") {
+          let rest = properties.tail;
+          loop$properties = rest;
+          loop$styles = styles;
+        } else {
+          let rest = properties.tail;
+          let name$1 = $;
+          let value$1 = $1;
+          loop$properties = rest;
+          loop$styles = styles + name$1 + ":" + value$1 + ";";
+        }
+      }
+    }
+  }
+}
+function styles(properties) {
+  return attribute2("style", do_styles(properties, ""));
+}
 function href(url) {
   return attribute2("href", url);
 }
@@ -2384,6 +2431,9 @@ function name(element_name) {
 function placeholder(text) {
   return attribute2("placeholder", text);
 }
+function rows(value) {
+  return attribute2("rows", to_string(value));
+}
 function type_(control_type) {
   return attribute2("type", control_type);
 }
@@ -2688,7 +2738,7 @@ function map6(element3, f) {
 function text3(content) {
   return text2(content);
 }
-function style(attrs, css) {
+function style2(attrs, css) {
   return unsafe_raw_html("", "style", attrs, css);
 }
 function article(attrs, children) {
@@ -2739,6 +2789,9 @@ function form(attrs, children) {
 function input(attrs) {
   return element2("input", attrs, empty_list);
 }
+function textarea(attrs, content) {
+  return element2("textarea", prepend(property2("value", string4(content)), attrs), toList([text2(content)]));
+}
 
 // build/dev/javascript/lustre/lustre/vdom/patch.mjs
 class Patch extends CustomType {
@@ -7232,7 +7285,10 @@ function task_view(task) {
         class$("delete-task")
       ]), toList([text3("Delete")]))
     ])),
-    p(toList([class$("task-description")]), toList([text3(task.description)])),
+    p(toList([
+      class$("task-description"),
+      style("white-space", "pre-wrap")
+    ]), toList([text3(task.description)])),
     div(toList([class$("task-details")]), toList([
       div(toList([class$("task-person")]), toList([
         small(List$Empty$const, toList([text3("Owner")])),
@@ -7290,6 +7346,12 @@ function project_view(_, project) {
     ]))
   ]), toList([
     form(toList([
+      class$("task-form"),
+      styles(toList([
+        ["display", "grid"],
+        ["grid-template-columns", "repeat(4, minmax(0, 1fr))"],
+        ["align-items", "start"]
+      ])),
       on_submit((fields) => {
         return new TaskSubmitted(project.id, fields);
       })
@@ -7298,13 +7360,30 @@ function project_view(_, project) {
       text_input("assignee", "Owner"),
       text_input("assignees", "Assignees"),
       text_input("due_date", "Due (MM/DD/YY)"),
-      text_input("description", "Details"),
-      button(List$Empty$const, toList([text3("Add task")]))
+      textarea(toList([
+        class$("task-description-input"),
+        styles(toList([
+          ["grid-column", "1 / -1"],
+          ["width", "100%"],
+          ["box-sizing", "border-box"],
+          ["min-height", "96px"],
+          ["resize", "vertical"],
+          ["padding", "12px 13px"],
+          ["border", "1px solid #d9cfc2"],
+          ["background", "#fffcf8"],
+          ["color", "#28231e"],
+          ["font", "inherit"]
+        ])),
+        name("description"),
+        placeholder("Details"),
+        rows(4)
+      ]), ""),
+      button(toList([class$("task-submit")]), 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;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 styles2() {
+  return style2(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:rgb(255, 100, 0);font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:rgb(255, 100, 0);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:#000000;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:rgb(255, 100, 0);color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:rgb(255, 100, 0);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) {
@@ -7319,7 +7398,7 @@ function dashboard_view(model) {
   if ($ instanceof Some) {
     let board = $[0];
     return main(toList([class$("page")]), append(toList([
-      styles(),
+      styles2(),
       header(List$Empty$const, toList([
         div(List$Empty$const, toList([
           text3("FRC 972 · IRON CLAW"),
@@ -7345,7 +7424,7 @@ function dashboard_view(model) {
     })));
   } else {
     return main(toList([class$("auth")]), toList([
-      h1(List$Empty$const, toList([text3("Loading Iron Claw...")])),
+      h1(List$Empty$const, toList([text3("Loading Iron Claw")])),
       error_view(model.error)
     ]));
   }
@@ -7368,10 +7447,12 @@ function creat_account_view(model, title2, submit, on_submit2) {
   }
   let link_text = _block$1;
   return main(toList([class$("auth")]), toList([
-    styles(),
+    styles2(),
     text3("FRC 972 · IRON CLAW"),
     h1(List$Empty$const, toList([text3(title2)])),
-    p(List$Empty$const, toList([text3("google sheets, more like shmoogle shits")])),
+    p(List$Empty$const, toList([
+      text3("google sheets, more like shmoogle shits (also passwords need to be at least 8 chars and username needs to be at least 3)")
+    ])),
     error_view(model.error),
     form(toList([on_submit(on_submit2)]), toList([
       input(toList([
@@ -7420,7 +7501,7 @@ function auth_view(model, title2, submit, on_submit2) {
   }
   let link_text = _block$1;
   return main(toList([class$("auth")]), toList([
-    styles(),
+    styles2(),
     text3("Team 972 · IRON CLAW"),
     h1(List$Empty$const, toList([text3(title2)])),
     p(List$Empty$const, toList([text3("google sheets, more like shmoogle shits")])),
@@ -7449,17 +7530,64 @@ function auth_view(model, title2, submit, on_submit2) {
 function view(model) {
   let $ = model.page;
   if ($ === "login") {
-    return auth_view(model, "Welcome back", "Log in", (var0) => {
+    return auth_view(model, "Greetings Comrade", "Log in", (var0) => {
       return new LoginSubmitted(var0);
     });
   } else if ($ === "register") {
-    return creat_account_view(model, "Create your account", "Create account", (var0) => {
+    return creat_account_view(model, "Create account", "Create account", (var0) => {
       return new RegisterSubmitted(var0);
     });
   } else {
     return dashboard_view(model);
   }
 }
+function remove_project(model, project_id) {
+  let $ = model.dashboard;
+  if ($ instanceof Some) {
+    let board = $[0];
+    let projects = filter(board.projects, (project) => {
+      return project.id !== project_id;
+    });
+    return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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);
+  } else {
+    return model;
+  }
+}
+function remove_task(model, task_id) {
+  let $ = model.dashboard;
+  if ($ instanceof Some) {
+    let board = $[0];
+    let projects = map2(board.projects, (project) => {
+      let tasks = filter(project.tasks, (task) => {
+        return task.id !== task_id;
+      });
+      return new Project(project.id, project.name, project.description, tasks);
+    });
+    return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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);
+  } else {
+    return model;
+  }
+}
+function update_task_status(model, task_id, status) {
+  let $ = model.dashboard;
+  if ($ instanceof Some) {
+    let board = $[0];
+    let projects = map2(board.projects, (project) => {
+      let tasks = map2(project.tasks, (task) => {
+        let $1 = task.id === task_id;
+        if ($1) {
+          return new Task(task.id, task.project_id, task.title, task.description, status, task.assignee, task.assignees, task.due_date);
+        } else {
+          return task;
+        }
+      });
+      return new Project(project.id, project.name, project.description, tasks);
+    });
+    return new Model(model.page, new Some(new Dashboard(board.username, projects)), 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);
+  } else {
+    return model;
+  }
+}
 function field2(fields, name2) {
   let _pipe = key_find(fields, name2);
   return unwrap2(_pipe, "");
@@ -7512,7 +7640,7 @@ function update2(model, message2) {
     _block = new_project_json(_pipe);
     let body2 = _block;
     return [
-      model,
+      new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, "", "", model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date),
       post("/api/projects", body2, expect_any_response((result) => {
         return new Saved((() => {
           let _pipe$1 = result;
@@ -7530,7 +7658,7 @@ function update2(model, message2) {
     _block = new_task_json(_pipe);
     let body2 = _block;
     return [
-      model,
+      new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, "", "", "", "", ""),
       post("/api/projects/" + to_string(project_id) + "/tasks", body2, expect_any_response((result) => {
         return new Saved((() => {
           let _pipe$1 = result;
@@ -7549,7 +7677,7 @@ function update2(model, message2) {
     _block = status_change_json(_pipe);
     let body2 = _block;
     return [
-      model,
+      update_task_status(model, task_id, status),
       post("/api/tasks/" + to_string(task_id) + "/status", body2, expect_any_response((result) => {
         return new Saved((() => {
           let _pipe$1 = result;
@@ -7652,7 +7780,7 @@ function update2(model, message2) {
   } else if (message2 instanceof DeleteTask) {
     let task_id = message2[0];
     return [
-      model,
+      remove_task(model, task_id),
       delete$2("/api/tasks/" + to_string(task_id), object2(List$Empty$const), expect_any_response((result) => {
         return new Saved((() => {
           let _pipe = result;
@@ -7665,7 +7793,7 @@ function update2(model, message2) {
   } else if (message2 instanceof DeleteProject) {
     let project_id = message2[0];
     return [
-      model,
+      remove_project(model, project_id),
       delete$2("/api/projects/" + to_string(project_id), object2(List$Empty$const), expect_any_response((result) => {
         return new Saved((() => {
           let _pipe = result;