summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app.rb13
-rw-r--r--public/css/style.css8
-rw-r--r--views/admin/person_detail.erb56
3 files changed, 73 insertions, 4 deletions
diff --git a/app.rb b/app.rb
index 7ee7851..9e9cb9d 100644
--- a/app.rb
+++ b/app.rb
@@ -927,6 +927,11 @@ get '/admin/people/:uuid' do
.map { |k, v| { uuid: k, name: v['name'] || "(unnamed · #{(v['members'] || []).length})" } }
.sort_by { |x| x[:name].downcase }
+ @existing_names_json = regular
+ .select { |_, v| v['name'] }
+ .map { |k, v| { uuid: k, name: v['name'] } }
+ .to_json
+
erb :'admin/person_detail'
end
@@ -965,7 +970,13 @@ post '/admin/people/:uuid/move' do
data['people'] = people
atomic_write(PEOPLE_PATH, JSON.pretty_generate(data))
- people[src] ? redirect("/admin/people/#{src}") : redirect('/admin/people')
+ if to == 'new'
+ redirect "/admin/people/#{new_uid}"
+ elsif people[src]
+ redirect "/admin/people/#{src}"
+ else
+ redirect '/admin/people'
+ end
end
post '/admin/people/__pool__/blacklist_all' do
diff --git a/public/css/style.css b/public/css/style.css
index 877bdde..f522bff 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -495,6 +495,14 @@ tr.delete-marked td { background: rgba(192,57,43,.08); }
}
.pool-label { color: #c8a84a; font-weight: 500; }
+.new-person-hero {
+ display: flex; flex-direction: column; align-items: flex-start;
+ margin-bottom: 16px;
+}
+.new-person-hero .face-detail-thumb img {
+ width: 140px; height: 140px; border-radius: 8px;
+}
+
.face-detail-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
diff --git a/views/admin/person_detail.erb b/views/admin/person_detail.erb
index 77f27b7..7a1ea0a 100644
--- a/views/admin/person_detail.erb
+++ b/views/admin/person_detail.erb
@@ -26,11 +26,32 @@
<% unless @is_pool %>
<%# ── Name ──────────────────────────────────────────────────────────────── %>
<section style="margin-bottom:20px">
- <form method="post" action="/admin/people/<%= ERB::Util.url_encode(@uuid) %>" class="name-form">
- <input type="text" name="name" value="<%= ERB::Util.html_escape(@name.to_s) %>"
- placeholder="Enter name…" class="name-input" style="max-width:300px">
+ <% if @count == 1 && !@name %>
+ <% m = @members.first; rel = m['rel']; box = m['box'] %>
+ <% parts = rel.split('/'); fname = parts.last; dir_rel = parts[0..-2].join('/') %>
+ <% album_url = dir_rel.empty? ? '/browse/' : "/browse/#{ERB::Util.html_escape(dir_rel)}" %>
+ <div class="new-person-hero">
+ <div class="face-detail-thumb" data-thumb="/thumb/<%= ERB::Util.html_escape(rel) %>">
+ <a href="<%= album_url %>?photo=<%= ERB::Util.url_encode(fname) %>" target="_blank">
+ <img src="/face/<%= ERB::Util.html_escape(rel) %>?box=<%= ERB::Util.html_escape(box.join(',')) %>"
+ width="140" height="140">
+ </a>
+ </div>
+ <p class="update-hint" style="margin-top:8px">Hover to see full photo &middot; Click to open in album</p>
+ </div>
+ <% end %>
+ <form id="name-form" method="post" action="/admin/people/<%= ERB::Util.url_encode(@uuid) %>" class="name-form">
+ <input type="text" name="name" id="name-input"
+ value="<%= ERB::Util.html_escape(@name.to_s) %>"
+ placeholder="Enter name…" class="name-input" style="max-width:300px"
+ autocomplete="off">
<button type="submit" class="btn">Save Name</button>
</form>
+ <form id="merge-into-form" method="post"
+ action="/admin/people/<%= ERB::Util.url_encode(@uuid) %>/merge"
+ style="display:none">
+ <input type="hidden" name="into" id="merge-into-uuid">
+ </form>
</section>
<%# ── Merge entire cluster ──────────────────────────────────────────────── %>
@@ -131,6 +152,35 @@
preview.style.top = y + 'px';
}
+ // Duplicate name check
+ var existingNames = <%= @existing_names_json || '[]' %>;
+ var nameForm = document.getElementById('name-form');
+ var nameInput = document.getElementById('name-input');
+ if (nameForm && nameInput) {
+ var forceNew = false;
+ nameForm.addEventListener('submit', function (e) {
+ if (forceNew) return;
+ var val = nameInput.value.trim();
+ if (!val) return;
+ var match = existingNames.find(function (p) {
+ return p.name.toLowerCase() === val.toLowerCase();
+ });
+ if (match) {
+ e.preventDefault();
+ var msg = '"' + match.name + '" already exists.\n\n'
+ + 'OK — add this photo to ' + match.name + "'s cluster\n"
+ + 'Cancel — create a new separate person named "' + val + '"';
+ if (confirm(msg)) {
+ document.getElementById('merge-into-uuid').value = match.uuid;
+ document.getElementById('merge-into-form').submit();
+ } else {
+ forceNew = true;
+ nameForm.submit();
+ }
+ }
+ });
+ }
+
document.querySelectorAll('.face-detail-thumb').forEach(function (el) {
el.querySelector('img').style.cursor = 'zoom-in';
el.addEventListener('mouseenter', function (e) {