diff options
| author | Ken D'Ambrosio <ken@jots.org> | 2026-06-08 19:11:51 +0000 |
|---|---|---|
| committer | Ken D'Ambrosio <ken@jots.org> | 2026-06-08 19:11:51 +0000 |
| commit | 00f63c03b7c5de68aea6a2305886bc1953a722b6 (patch) | |
| tree | 054ff2f9029ea57c50da6e823982648f8766ec98 /views | |
| parent | 01f52565f460a0107679999588b73b770f01a98c (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.erb | 5 | ||||
| -rw-r--r-- | views/search.erb | 53 |
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 %> |
