Rename Django project module plantdb/ → bloombase/, update all references in manage.py, docker-compose.yml, wsgi/asgi/settings. Replace PlantDB with BloomBase in all template titles and navbar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
4.7 KiB
HTML
118 lines
4.7 KiB
HTML
{% extends "plants/base.html" %}
|
|
{% block title %}Identify plant — BloomBase{% endblock %}
|
|
{% block content %}
|
|
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<a href="{% url 'plant_list' %}" class="btn btn-sm btn-outline-secondary">←</a>
|
|
<h5 class="mb-0">Identify plant</h5>
|
|
</div>
|
|
|
|
<p class="text-muted small mb-3">
|
|
Add one or more photos, tag what each shows, then tap Identify.
|
|
</p>
|
|
|
|
{% if error %}
|
|
<div class="alert alert-warning py-2 small mb-3">{{ error }}</div>
|
|
{% endif %}
|
|
|
|
<form method="post" enctype="multipart/form-data" id="identify-form">
|
|
{% csrf_token %}
|
|
<input type="file" name="images" accept="image/jpeg,image/png" class="d-none" id="file-picker">
|
|
|
|
<div id="photo-rows" class="mb-3"></div>
|
|
|
|
<div id="add-btn-wrap" class="mb-3">
|
|
<label class="btn btn-outline-secondary w-100 py-3" for="file-picker" id="add-photo-label">
|
|
📷 Add photo <span id="photo-count" class="text-muted small"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-success w-100" id="identify-btn" disabled>
|
|
Identify →
|
|
</button>
|
|
</form>
|
|
|
|
<div class="text-center mt-3">
|
|
<a href="{% url 'plant_add' %}" class="text-muted small">Or enter name manually</a>
|
|
</div>
|
|
|
|
<div id="identify-spinner" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,.88); z-index:9999; align-items:center; justify-content:center; flex-direction:column; gap:12px;">
|
|
<div class="spinner-border text-success" role="status"><span class="visually-hidden">Identifying…</span></div>
|
|
<p class="text-muted small mb-0">Identifying plant…</p>
|
|
</div>
|
|
|
|
<script>
|
|
const ORGANS = [
|
|
{val: 'flower', label: '🌸 flower'},
|
|
{val: 'leaf', label: '🍃 leaf'},
|
|
{val: 'habit', label: '🌿 habit'},
|
|
{val: 'fruit', label: '🍎 fruit'},
|
|
{val: 'bark', label: '🪵 bark'},
|
|
{val: 'auto', label: '✨ auto'},
|
|
];
|
|
const selected = []; // {file, organ, objectUrl}
|
|
const picker = document.getElementById('file-picker');
|
|
|
|
picker.addEventListener('change', function () {
|
|
if (!this.files[0] || selected.length >= 5) return;
|
|
selected.push({file: this.files[0], organ: 'auto', objectUrl: URL.createObjectURL(this.files[0])});
|
|
this.value = '';
|
|
render();
|
|
});
|
|
|
|
function setOrgan(idx, organ) {
|
|
selected[idx].organ = organ;
|
|
render();
|
|
}
|
|
|
|
function removePhoto(idx) {
|
|
URL.revokeObjectURL(selected[idx].objectUrl);
|
|
selected.splice(idx, 1);
|
|
render();
|
|
}
|
|
|
|
function render() {
|
|
const rows = document.getElementById('photo-rows');
|
|
rows.innerHTML = '';
|
|
selected.forEach((item, i) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'd-flex gap-2 align-items-start mb-3';
|
|
row.innerHTML = `
|
|
<div style="position:relative;flex-shrink:0;">
|
|
<img src="${item.objectUrl}" style="width:72px;height:72px;object-fit:cover;border-radius:8px;" alt="">
|
|
<button type="button" onclick="removePhoto(${i})"
|
|
style="position:absolute;top:2px;right:2px;background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:50%;width:20px;height:20px;font-size:10px;cursor:pointer;padding:0;line-height:1;">✕</button>
|
|
</div>
|
|
<div style="flex:1;">
|
|
<div class="text-muted small mb-1">What's shown?</div>
|
|
<div class="d-flex flex-wrap gap-1">
|
|
${ORGANS.map(o => `
|
|
<span onclick="setOrgan(${i}, '${o.val}')"
|
|
style="cursor:pointer; padding:2px 8px; border-radius:10px; font-size:11px; ${item.organ === o.val ? 'background:#52b788;color:#fff;' : 'background:#e9ecef;color:#6c757d;'}">
|
|
${o.label}
|
|
</span>
|
|
`).join('')}
|
|
</div>
|
|
<input type="hidden" name="organs" value="${item.organ}" id="organ-${i}">
|
|
</div>`;
|
|
rows.appendChild(row);
|
|
});
|
|
|
|
document.querySelectorAll('input[name="organs"]').forEach((el, i) => {
|
|
if (selected[i]) el.value = selected[i].organ;
|
|
});
|
|
|
|
document.getElementById('photo-count').textContent = selected.length ? `(${selected.length}/5)` : '';
|
|
document.getElementById('add-btn-wrap').style.display = selected.length >= 5 ? 'none' : '';
|
|
document.getElementById('identify-btn').disabled = selected.length === 0;
|
|
}
|
|
|
|
document.getElementById('identify-form').addEventListener('submit', function (e) {
|
|
if (selected.length === 0) { e.preventDefault(); return; }
|
|
const dt = new DataTransfer();
|
|
selected.forEach(s => dt.items.add(s.file));
|
|
picker.files = dt.files;
|
|
document.getElementById('identify-spinner').style.display = 'flex';
|
|
});
|
|
</script>
|
|
{% endblock %}
|