summaryrefslogtreecommitdiffstats
path: root/views
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken@jots.org>2026-06-08 19:11:51 +0000
committerKen D'Ambrosio <ken@jots.org>2026-06-08 19:11:51 +0000
commit00f63c03b7c5de68aea6a2305886bc1953a722b6 (patch)
tree054ff2f9029ea57c50da6e823982648f8766ec98 /views
parent01f52565f460a0107679999588b73b770f01a98c (diff)
Add photo search with Boolean operators
- Server-side search index built from all album.json files + people.json, cached in memory for 5 minutes. Each photo document includes filename, album path words, title, caption, camera, date parts (year/month-name/ full date), and person names. - Recursive-descent Boolean parser: AND (explicit or implicit between consecutive terms), OR, NOT, with standard precedence. - GET /search?q=... returns a photo grid (max 300 results) linking each photo back to its album lightbox. - Search box added to the site header; hidden on mobile. - Results show filename, date, person names, and album path per photo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'views')
-rw-r--r--views/layout.erb5
-rw-r--r--views/search.erb53
2 files changed, 58 insertions, 0 deletions
diff --git a/views/layout.erb b/views/layout.erb
index e0c5399..7069e76 100644
--- a/views/layout.erb
+++ b/views/layout.erb
@@ -16,6 +16,11 @@
<body>
<header class="site-header">
<a href="/browse/" class="site-logo">Albumen</a>
+ <form action="/search" method="get" class="header-search">
+ <input type="search" name="q" placeholder="Search…" autocomplete="off"
+ value="<%= defined?(@search_query) ? ERB::Util.html_escape(@search_query.to_s) : '' %>"
+ class="header-search-input">
+ </form>
<nav class="site-nav">
<% if FACES_ENABLED %><a href="/people">People</a><% end %>
<% if admin? %>
diff --git a/views/search.erb b/views/search.erb
new file mode 100644
index 0000000..1010284
--- /dev/null
+++ b/views/search.erb
@@ -0,0 +1,53 @@
+<div class="album-header">
+ <h1>Search</h1>
+ <form action="/search" method="get" class="search-form">
+ <input type="search" name="q" value="<%= ERB::Util.html_escape(@search_query.to_s) %>"
+ placeholder="e.g. ken 2004 OR carol AND NOT vacation"
+ class="search-input" autofocus autocomplete="off">
+ <button type="submit" class="btn">Search</button>
+ </form>
+ <p class="search-hint">
+ Searches filename, album, caption, date, and people names.
+ Consecutive terms are AND'd. Operators: <code>AND</code> <code>OR</code> <code>NOT</code>.
+ Example: <code>ken OR carol AND NOT vacation</code>
+ </p>
+</div>
+
+<% if @search_query && !@search_query.empty? %>
+ <% if @results %>
+ <p class="update-hint">
+ <%= @total %> result<%= @total == 1 ? '' : 's' %>
+ <% if @total > @results.length %>(showing first <%= @results.length %>)<% end %>
+ </p>
+ <% if @results.empty? %>
+ <p class="empty-album">No photos matched "<%= ERB::Util.html_escape(@search_query) %>".</p>
+ <% else %>
+ <div class="grid">
+ <% @results.each do |r| %>
+ <% album_url = r[:dir_rel].empty? ? '/browse/' : "/browse/#{ERB::Util.html_escape(r[:dir_rel])}" %>
+ <a href="<%= album_url %>?photo=<%= ERB::Util.url_encode(r[:filename]) %>"
+ class="card search-card" style="text-decoration:none">
+ <div class="thumb-wrap">
+ <img src="/thumb/<%= ERB::Util.html_escape(r[:rel]) %>" loading="lazy"
+ alt="<%= ERB::Util.html_escape(r[:filename]) %>">
+ </div>
+ <div class="card-meta">
+ <div class="filename"><%= ERB::Util.html_escape(r[:filename]) %></div>
+ <% if r[:taken_at] %>
+ <div class="card-caption"><%= ERB::Util.html_escape(r[:taken_at][0..9]) %></div>
+ <% end %>
+ <% unless r[:people].empty? %>
+ <div class="card-caption"><%= ERB::Util.html_escape(r[:people].join(', ')) %></div>
+ <% end %>
+ <div class="search-album-path">
+ <a href="<%= album_url %>" onclick="event.stopPropagation()">
+ <%= ERB::Util.html_escape(r[:dir_rel].empty? ? '(root)' : r[:dir_rel]) %>
+ </a>
+ </div>
+ </div>
+ </a>
+ <% end %>
+ </div>
+ <% end %>
+ <% end %>
+<% end %>