Replace user settings panel with standalone page
Same pattern as the series show page: full-page layout with section
header, Edit/Save toggle, and detail-fields. Removed the old panel
wrapper and form views. The hidden form holds just the CSRF token;
the email input links to it via the form attribute.
Assisted-by: Claude Opus 4.6 via Claude Code
diff --git a/lib/ketchup/views/series/show.rb b/lib/ketchup/views/series/show.rb
index 97f146b..96e62c4 100644
--- a/lib/ketchup/views/series/show.rb
+++ b/lib/ketchup/views/series/show.rb
@@ -36,7 +36,7 @@ module Views
"x-show": "editing",
"x-on:click": "editing = false; $dispatch('stop-editing')"
) do
- plain "Done"
+ plain "Save"
end
end
diff --git a/lib/ketchup/views/user/show.rb b/lib/ketchup/views/user/show.rb
new file mode 100644
index 0000000..564d06d
--- /dev/null
+++ b/lib/ketchup/views/user/show.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "phlex"
+
+require_relative "../layout"
+
+module Views
+ module User
+ class Show < Phlex::HTML
+ def initialize(current_user:, csrf:)
+ @current_user = current_user
+ @csrf = csrf
+ end
+
+ def view_template
+ email_path = "/users/#{@current_user[:id]}/email"
+ email = @current_user[:email]
+
+ render Layout.new(current_user: @current_user, title: "Settings — Ketchup", active_view: nil) do
+ div(class: "dashboard") do
+ div(class: "main-column") do
+ section(class: "section", "x-data": "{ editing: false }") do
+ div(class: "section-header") do
+ h2(class: "section-title") do
+ span(class: "section-title-text") { "User Settings" }
+ end
+ button(
+ class: "section-edit-btn",
+ "x-show": "!editing",
+ "x-on:click": "editing = true"
+ ) do
+ plain "Edit"
+ end
+ button(
+ class: "section-edit-btn",
+ "x-show": "editing",
+ "x-cloak": true,
+ "x-on:click": "editing = false; document.getElementById('user-form').requestSubmit()"
+ ) do
+ plain "Save"
+ end
+ end
+
+ form(method: "post", action: email_path, id: "user-form", style: "display:none") do
+ input(type: "hidden", name: "_csrf", value: @csrf.call(email_path))
+ end
+
+ dl(class: "detail-fields") do
+ dt { "Login" }
+ dd { @current_user[:login] }
+
+ dt { "Email" }
+ dd("x-show": "!editing") do
+ if email
+ plain email
+ else
+ span(class: "detail-placeholder") { "not set" }
+ end
+ end
+ dd("x-show": "editing", "x-cloak": true) do
+ input(
+ type: "email", name: "email",
+ form: "user-form",
+ class: "detail-input",
+ value: email,
+ placeholder: "for notifications"
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ketchup/views/user_form.rb b/lib/ketchup/views/user_form.rb
deleted file mode 100644
index 79eee2b..0000000
--- a/lib/ketchup/views/user_form.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-module Views
- class UserForm < Phlex::HTML
- def initialize(current_user:, csrf:)
- @current_user = current_user
- @csrf = csrf
- end
-
- def view_template
- email_path = "/users/#{@current_user[:id]}/email"
- email = @current_user[:email]
-
- div(class: "panel-inner", "x-data": "{ editing: false }") do
- div(class: "panel-header") do
- a(href: "/", class: "panel-close", "aria-label": "Close") { "←" }
- div(class: "panel-actions") do
- button(
- class: "panel-action",
- "x-show": "!editing",
- "x-on:click": "editing = true"
- ) { "Edit" }
- button(
- type: "submit",
- form: "user-form",
- class: "panel-action",
- "x-show": "editing",
- "x-cloak": true
- ) { "Save" }
- end
- end
-
- div(class: "panel-body") do
- h2(class: "panel-body-title") { @current_user[:login] }
-
- form(method: "post", action: email_path, id: "user-form") do
- input(type: "hidden", name: "_csrf", value: @csrf.call(email_path))
-
- dl(class: "detail-fields") do
- dt { "Email" }
- dd("x-show": "!editing") do
- if email
- plain email
- else
- span(class: "detail-placeholder") { "not set" }
- end
- end
- dd("x-show": "editing", "x-cloak": true) do
- input(
- type: "email", name: "email",
- class: "detail-input",
- value: email,
- placeholder: "for notifications"
- )
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/ketchup/views/user_panel.rb b/lib/ketchup/views/user_panel.rb
deleted file mode 100644
index 405c0e2..0000000
--- a/lib/ketchup/views/user_panel.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-require_relative "user_form"
-
-module Views
- class UserPanel < Phlex::HTML
- def initialize(current_user:, csrf:)
- @current_user = current_user
- @csrf = csrf
- end
-
- def view_template
- render UserForm.new(current_user: @current_user, csrf: @csrf)
- end
- end
-end
diff --git a/lib/ketchup/web.rb b/lib/ketchup/web.rb
index 5c75eae..27e6ddc 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -10,7 +10,7 @@ require_relative "views/calendar"
require_relative "views/agenda"
require_relative "views/series/new"
require_relative "views/series/show"
-require_relative "views/user_panel"
+require_relative "views/user/show"
class Web < Roda
plugin :halt
@@ -77,12 +77,8 @@ class Web < Roda
r.on "users", Integer do |user_id|
r.halt 404 unless @user.id == user_id
- r.get "panel" do
- Views::UserPanel.new(current_user: @user, csrf: method(:csrf_token)).call
- end
-
r.get do
- Views::Dashboard.new(current_user: @user, csrf: method(:csrf_token)).call
+ Views::User::Show.new(current_user: @user, csrf: method(:csrf_token)).call
end
r.post "email" do
diff --git a/test/test_web.rb b/test/test_web.rb
index 0ef34ae..11b77e4 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -423,31 +423,22 @@ class TestWeb < Minitest::Test
assert_equal Date.new(2026, 3, 1), task[:due_date]
end
- def test_get_user_page_shows_dashboard
+ def test_get_user_page_shows_settings
get "/", {}, auth_headers # create user
user_id = DB[:users].first(login: "alice@example.com")[:id]
get "/users/#{user_id}", {}, auth_headers
assert last_response.ok?
assert_includes last_response.body, "alice@example.com"
- end
-
- def test_get_user_panel_shows_email_form
- get "/", {}, auth_headers # create user
- user_id = DB[:users].first(login: "alice@example.com")[:id]
-
- get "/users/#{user_id}/panel", {}, auth_headers
- assert last_response.ok?
- assert_includes last_response.body, "alice@example.com"
+ assert_includes last_response.body, "Settings"
assert_includes last_response.body, 'name="email"'
- refute_includes last_response.body, "<!DOCTYPE"
end
def test_post_user_email
get "/", {}, auth_headers # create user
user_id = DB[:users].first(login: "alice@example.com")[:id]
- get "/users/#{user_id}/panel", {}, auth_headers
+ get "/users/#{user_id}", {}, auth_headers
token = last_response.body[/name="_csrf" value="([^"]+)"/, 1]
post "/users/#{user_id}/email", { "_csrf" => token, email: "alice@example.org" }, auth_headers
assert last_response.redirect?
@@ -460,7 +451,7 @@ class TestWeb < Minitest::Test
get "/", {}, auth_headers
user_id = DB[:users].first(login: "alice@example.com")[:id]
- get "/users/#{user_id}/panel", {}, auth_headers
+ get "/users/#{user_id}", {}, auth_headers
token = last_response.body[/name="_csrf" value="([^"]+)"/, 1]
post "/users/#{user_id}/email", { "_csrf" => token, email: "" }, auth_headers