Address review: drop render_ prefixes, extract RelativeTime, use Pathname
- Rename the Home view partials to drop the render_ prefix; name them by
role (topbar, recent_assets, save_panel, dock, ...) since header/main
would otherwise shadow Phlex's element methods.
- Move relative-time formatting out of the view into a standalone,
unit-tested Domus::RelativeTime.
- Build icon paths with Pathname instead of File, matching app.rb.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147qRctyhELxJgGs3nHizEM
diff --git a/lib/relative_time.rb b/lib/relative_time.rb
new file mode 100644
index 0000000..e169732
--- /dev/null
+++ b/lib/relative_time.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Domus
+ # Formats a timestamp as a compact, archival relative age —
+ # "now", "5m", "3h", "2d", "1w", "4mo", "2y".
+ module RelativeTime
+ module_function
+
+ def format(at, now: Time.now)
+ return "" unless at
+
+ seconds = (now - at).to_i
+ return "now" if seconds < 60
+
+ minutes = seconds / 60
+ return "#{minutes}m" if minutes < 60
+
+ hours = minutes / 60
+ return "#{hours}h" if hours < 24
+
+ days = hours / 24
+ return "#{days}d" if days < 7
+
+ weeks = days / 7
+ return "#{weeks}w" if days < 30
+
+ months = days / 30
+ return "#{months}mo" if months < 12
+
+ "#{days / 365}y"
+ end
+ end
+end
diff --git a/lib/views/home.rb b/lib/views/home.rb
index 2d0374d..5237e97 100644
--- a/lib/views/home.rb
+++ b/lib/views/home.rb
@@ -3,13 +3,14 @@
require "phlex"
require_relative "icons"
require_relative "capture_form"
+require_relative "../relative_time"
module Domus
module Views
- # The home page — the archive's front door. Capture actions live in the
- # header (and a thumb-reachable dock on small screens) and open the
- # picker in place: the whole page is one captureApp() Alpine component,
- # so choosing a file swaps the recent-asset list for the save form.
+ # The home page — the archive's front door. Capture actions live in a
+ # thumb-reachable dock and open the picker in place: the whole page is one
+ # captureApp() Alpine component, so choosing a file swaps the recent-asset
+ # list for the save form.
class Home < Phlex::HTML
include Icons
@@ -31,10 +32,10 @@ module Domus
end
body do
div(class: "page", "x-data": "captureApp()") do
- render_header
- render_main
- render_capture
- render_dock
+ topbar
+ recent_assets
+ save_panel
+ dock
end
end
end
@@ -42,7 +43,7 @@ module Domus
private
- def render_header
+ def topbar
header(class: "topbar") do
a(href: "/", class: "logo") do
span(class: "logo-mark")
@@ -55,7 +56,7 @@ module Domus
# captureApp() chooses for the viewport (upload on desktop, camera on
# mobile), and the caret opens a drop-up menu with the alternate.
# Both labels are rendered and toggled by media query.
- def render_capture_control
+ def capture_control
div(class: "capture", "@click.outside": "menuOpen = false") do
div(class: "split") do
button(
@@ -97,7 +98,7 @@ module Domus
end
end
- def render_main
+ def recent_assets
main(class: "wrap", "x-show": "state === 'capture'") do
section do
div(class: "sec-h") do
@@ -105,15 +106,15 @@ module Domus
span(class: "sub") { plain "#{@total} tracked" } if @total.positive?
end
if @assets.empty?
- render_empty
+ empty_state
else
- render_archive
+ archive
end
end
end
end
- def render_archive
+ def archive
div(class: "archive") do
@assets.each do |asset|
div(class: "entry") do
@@ -121,20 +122,20 @@ module Domus
div(class: "body") do
div(class: "nm") { plain asset[:name] }
end
- span(class: "time") { plain relative_time(asset[:created_at]) }
+ span(class: "time") { plain RelativeTime.format(asset[:created_at]) }
end
end
end
end
- def render_empty
+ def empty_state
div(class: "archive empty") do
p(class: "empty-line") { plain "Nothing tracked yet." }
end
end
# The capture save step, shown once a photo or file has been chosen.
- def render_capture
+ def save_panel
div(class: "content", "x-show": "state === 'saved'", "x-cloak": true) do
div(class: "card") do
render CaptureForm.new
@@ -145,36 +146,11 @@ module Domus
# The capture front door: a dock fixed to the bottom of the viewport on
# every screen, so the primary action stays reachable and out of the
# header.
- def render_dock
+ def dock
div(class: "dock", "x-show": "state === 'capture'") do
- render_capture_control
+ capture_control
end
end
-
- # Compact, archival relative time — "now", "5m", "3h", "2d", "1w", "4mo".
- def relative_time(at)
- return "" unless at
-
- seconds = (Time.now - at).to_i
- return "now" if seconds < 60
-
- minutes = seconds / 60
- return "#{minutes}m" if minutes < 60
-
- hours = minutes / 60
- return "#{hours}h" if hours < 24
-
- days = hours / 24
- return "#{days}d" if days < 7
-
- weeks = days / 7
- return "#{weeks}w" if days < 30
-
- months = days / 30
- return "#{months}mo" if months < 12
-
- "#{days / 365}y"
- end
end
end
end
diff --git a/lib/views/icons.rb b/lib/views/icons.rb
index 7d86983..35636e5 100644
--- a/lib/views/icons.rb
+++ b/lib/views/icons.rb
@@ -1,15 +1,17 @@
# frozen_string_literal: true
+require "pathname"
+
module Domus
module Views
# Inlines the SVG files from public/icons so they inherit currentColor
# and can be sized and recoloured with CSS. Mixed into the Phlex views
# that draw icons.
module Icons
- ICONS_DIR = File.expand_path("../../public/icons", __dir__)
+ ICONS_DIR = Pathname(__dir__).join("../../public/icons").expand_path
ICONS = Hash.new do |cache, name|
- cache[name] = File.read(File.join(ICONS_DIR, "#{name}.svg")).freeze
+ cache[name] = ICONS_DIR.join("#{name}.svg").read.freeze
end
def icon(name)
diff --git a/test/test_relative_time.rb b/test/test_relative_time.rb
new file mode 100644
index 0000000..729434c
--- /dev/null
+++ b/test/test_relative_time.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+require_relative "../lib/relative_time"
+
+class TestRelativeTime < Minitest::Test
+ def fmt(seconds_ago)
+ now = Time.now
+ Domus::RelativeTime.format(now - seconds_ago, now: now)
+ end
+
+ def test_blank_for_nil
+ assert_equal "", Domus::RelativeTime.format(nil)
+ end
+
+ def test_now_under_a_minute
+ assert_equal "now", fmt(30)
+ end
+
+ def test_minutes
+ assert_equal "5m", fmt(5 * 60)
+ end
+
+ def test_hours
+ assert_equal "3h", fmt(3 * 60 * 60)
+ end
+
+ def test_days
+ assert_equal "2d", fmt(2 * 86_400)
+ end
+
+ def test_weeks
+ assert_equal "1w", fmt(8 * 86_400)
+ end
+
+ def test_months
+ assert_equal "2mo", fmt(70 * 86_400)
+ end
+
+ def test_years
+ assert_equal "1y", fmt(400 * 86_400)
+ end
+end