Two-column homepage with task list and new series form
Replaces the standalone new-series page with a single home view.
Tasks column shows active tasks ordered by due date with overdue
styling; form column creates new series.

Assisted-by: Claude Opus 4.6 via pi
change topxponmlqvommnrqmvvwpvnnsmqxrou
commit c77a612412fc869e142d4b81a455f75ff0d20d7c
author Alpha Chen <alpha@kejadlen.dev>
date
parent qxmtovwl
diff --git a/lib/views/home.rb b/lib/views/home.rb
new file mode 100644
index 0000000..27622ef
--- /dev/null
+++ b/lib/views/home.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "phlex"
+
+require_relative "layout"
+
+module Views
+  class Home < Phlex::HTML
+    def initialize(current_user:, tasks:)
+      @current_user = current_user
+      @tasks = tasks
+    end
+
+    def view_template
+      render Layout.new(current_user: @current_user) do
+        div(class: "home") do
+          div(class: "panel flow") do
+            h2 { "Tasks" }
+
+            if @tasks.empty?
+              p(class: "empty") { "No tasks yet." }
+            else
+              ul(class: "task-list") do
+                @tasks.each do |task|
+                  li(class: "task-item") do
+                    task_card(task)
+                  end
+                end
+              end
+            end
+          end
+
+          div(class: "panel flow") do
+            h2 { "New Series" }
+
+            form(method: "post", action: "/series") do
+              div(class: "field") do
+                label(for: "note") { "Note" }
+                textarea(id: "note", name: "note", rows: 3, required: true)
+              end
+
+              div(class: "field") do
+                label(for: "interval_count") { "Repeat every" }
+                div(class: "interval") do
+                  input(
+                    type: "number", id: "interval_count", name: "interval_count",
+                    min: 1, value: 1, required: true
+                  )
+                  select(id: "interval_unit", name: "interval_unit", required: true) do
+                    option(value: "day") { "day(s)" }
+                    option(value: "week") { "week(s)" }
+                    option(value: "month") { "month(s)" }
+                    option(value: "quarter") { "quarter(s)" }
+                    option(value: "year") { "year(s)" }
+                  end
+                end
+              end
+
+              div(class: "field") do
+                label(for: "first_due_date") { "First due date" }
+                input(
+                  type: "date", id: "first_due_date", name: "first_due_date",
+                  value: Date.today.to_s, required: true
+                )
+              end
+
+              button(type: "submit") { "Create" }
+            end
+          end
+        end
+      end
+    end
+
+    private
+
+    def task_card(task)
+      name = task[:note].lines.first&.strip || task[:note]
+      overdue = task[:due_date] < Date.today
+
+      div(class: ["task-card", ("task-overdue" if overdue)]) do
+        span(class: "task-name") { name }
+        span(class: "task-due") do
+          plain overdue ? "Overdue — due #{task[:due_date]}" : "Due #{task[:due_date]}"
+        end
+        span(class: "task-interval") do
+          count = task[:interval_count]
+          unit = task[:interval_unit]
+          plain "Every #{count} #{count == 1 ? unit : "#{unit}s"}"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/web.rb b/lib/web.rb
index 485c3e3..90752eb 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -3,7 +3,7 @@
 require "roda"
 
 require_relative "db"
-require_relative "views/series/new"
+require_relative "views/home"
 
 class Web < Roda
   plugin :halt
@@ -29,7 +29,22 @@ class Web < Roda
     r.halt 403 unless current_user
 
     r.root do
-      Views::Series::New.new(current_user:).call
+      tasks = DB[:tasks]
+        .join(:series, id: :series_id)
+        .where(completed_at: nil)
+        .select(
+          Sequel[:tasks][:id],
+          Sequel[:tasks][:due_date],
+          Sequel[:tasks][:completed_at],
+          Sequel[:series][:note],
+          Sequel[:series][:interval_unit],
+          Sequel[:series][:interval_count],
+          Sequel[:series][:user_id]
+        )
+        .order(Sequel[:tasks][:due_date])
+        .all
+
+      Views::Home.new(current_user:, tasks:).call
     end
 
     r.on "series" do
diff --git a/public/css/app.css b/public/css/app.css
index 0aba9cd..9c3ba22 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -57,6 +57,78 @@ h1 { font-size: var(--step-2); }
   padding: var(--space-l) var(--grid-gutter);
 }
 
+/* -------------------- */
+/* Home layout */
+/* -------------------- */
+
+.home {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: var(--space-l);
+  padding: var(--space-l) var(--grid-gutter);
+  max-width: 60rem;
+  margin-inline: auto;
+}
+
+@media (min-width: 48rem) {
+  .home {
+    grid-template-columns: 1fr 1fr;
+  }
+}
+
+.panel {
+  padding: var(--space-m);
+}
+
+/* -------------------- */
+/* Task list */
+/* -------------------- */
+
+.task-list {
+  list-style: none;
+  padding: 0;
+  display: grid;
+  gap: var(--space-s);
+}
+
+.task-card {
+  display: grid;
+  gap: var(--space-3xs);
+  padding: var(--space-s);
+  border: 1px solid #e0e0e0;
+  border-radius: 6px;
+  background: #fff;
+}
+
+.task-card.task-overdue {
+  border-color: #c0392b;
+  background: #fdf2f2;
+}
+
+.task-name {
+  font-weight: 600;
+}
+
+.task-due {
+  font-size: var(--step--1);
+  color: #666;
+}
+
+.task-overdue .task-due {
+  color: #c0392b;
+  font-weight: 600;
+}
+
+.task-interval {
+  font-size: var(--step--1);
+  color: #888;
+}
+
+.empty {
+  color: #888;
+  font-style: italic;
+}
+
 /* -------------------- */
 /* Forms */
 /* -------------------- */
diff --git a/test/test_web.rb b/test/test_web.rb
index 782cb0e..db7becb 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -25,6 +25,23 @@ class TestWeb < Minitest::Test
     assert_includes last_response.body, 'name="note"'
     assert_includes last_response.body, 'name="interval_count"'
     assert_includes last_response.body, 'name="interval_unit"'
+    assert_includes last_response.body, 'name="first_due_date"'
+  end
+
+  def test_root_shows_empty_state
+    get "/", {}, tailscale_headers
+    assert_includes last_response.body, "No tasks yet."
+  end
+
+  def test_root_shows_active_tasks
+    post "/series", {
+      note: "Call Mom", interval_unit: "week", interval_count: "2",
+      first_due_date: "2026-03-01"
+    }, tailscale_headers
+
+    get "/", {}, tailscale_headers
+    assert_includes last_response.body, "Call Mom"
+    assert_includes last_response.body, "2026-03-01"
   end
 
   def test_root_shows_current_user