fix: Bild-Upload pro Artikel-Zeile — Datei wählen ohne Dateinamen tippen
- Jede Artikel-Zeile hat jetzt einen 📷 Upload-Button (row-upload-btn)
- Klick → native File-Picker → Auto-Upload mit dem korrekten Artikel-Dateinamen
- uploadForRow() JS-Funktion: async fetch + Badge-Update + Toast-Feedback
- Bugfix: f.write(file_data) statt f.write(data) im _handle_upload
- Upload-Zone oben bleibt für Bulk/Drag-Drop erhalten
This commit is contained in:
+54
-2
@@ -225,7 +225,7 @@ def build_index(articles: list[dict]) -> str:
|
||||
ready_dot(image_ok, "Bild")
|
||||
)
|
||||
|
||||
items_html += f"""<div class="article-row" data-ready="{str(all_ready).lower()}">
|
||||
items_html += f"""<div class="article-row" data-ready="{str(all_ready).lower()}" data-filename="{a['filename']}">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" class="article-check" value="{a['filename']}">
|
||||
<span class="checkmark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
|
||||
@@ -237,6 +237,10 @@ def build_index(articles: list[dict]) -> str:
|
||||
</div>
|
||||
<span class="readiness">{readiness}</span>
|
||||
{image_badge}
|
||||
<label class="row-upload-btn" title="Bild für diesen Artikel hochladen">
|
||||
<input type="file" accept="image/*" class="row-file-input" onchange="uploadForRow(this, '{a['filename']}')" hidden>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</label>
|
||||
</div>\n"""
|
||||
items_html += "</div>\n"
|
||||
|
||||
@@ -334,6 +338,14 @@ def build_index(articles: list[dict]) -> str:
|
||||
.upload-result {{ margin-top: 0.8rem; font-size: 0.82rem; font-family: monospace; word-break: break-all; padding: 0.5rem; background: #f0fdf4; border-radius: 6px; display: none; }}
|
||||
.upload-result.show {{ display: block; }}
|
||||
|
||||
.row-upload-btn {{ display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; color: var(--accent); background: var(--accent-ghost); border: 1px solid transparent; transition: all 0.15s; flex-shrink: 0; margin-left: 0.25rem; }}
|
||||
.row-upload-btn:hover {{ background: var(--accent); color: #fff; border-color: var(--accent); }}
|
||||
.row-upload-btn:active {{ transform: scale(0.93); }}
|
||||
.row-upload-btn.uploading {{ background: var(--success); color: #fff; pointer-events: none; animation: pulse 0.8s infinite; }}
|
||||
@keyframes pulse {{ 0%,100%{{opacity:1}} 50%{{opacity:0.5}} }}
|
||||
|
||||
.row-file-input {{ display: none; }}
|
||||
|
||||
.toast {{ position: fixed; bottom: 2rem; right: 2rem; background: var(--success); color: #fff; padding: 0.8rem 1.6rem; border-radius: 10px; font-weight: 500; font-family: var(--font); box-shadow: 0 6px 24px rgba(16,185,129,0.3); opacity: 0; transform: translateY(16px); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); pointer-events: none; z-index: 1000; }}
|
||||
.toast.show {{ opacity: 1; transform: translateY(0); }}
|
||||
.toast.error {{ background: var(--danger); box-shadow: 0 6px 24px rgba(192,57,43,0.3); }}
|
||||
@@ -468,6 +480,46 @@ def build_index(articles: list[dict]) -> str:
|
||||
}});
|
||||
}}
|
||||
|
||||
async function uploadForRow(input, articleFile) {{
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
const btn = input.closest('.row-upload-btn');
|
||||
if (btn) btn.classList.add('uploading');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('article', articleFile);
|
||||
|
||||
try {{
|
||||
const resp = await fetch('/upload?key={ACCESS_TOKEN}', {{ method: 'POST', body: formData }});
|
||||
const data = await resp.json();
|
||||
if (btn) btn.classList.remove('uploading');
|
||||
if (data.ok) {{
|
||||
showToast('✅ ' + file.name + ' → ' + articleFile.split('_2026')[0].replace(/_/g,' ').replace('artikel ',''), false);
|
||||
// Update image badge (bump count visually)
|
||||
const row = input.closest('.article-row');
|
||||
if (row) {{
|
||||
const badge = row.querySelector('.img-badge');
|
||||
if (badge) {{
|
||||
const match = badge.textContent.match(/\\d+/);
|
||||
const current = match ? parseInt(match[0]) : 0;
|
||||
badge.textContent = '🖼' + (current + 1);
|
||||
}} else {{
|
||||
const info = row.querySelector('.article-info');
|
||||
if (info) info.insertAdjacentHTML('afterend', '<span class=\"img-badge\">🖼1</span>');
|
||||
}}
|
||||
}}
|
||||
// Clear the file input so the same file can be re-uploaded
|
||||
input.value = '';
|
||||
}} else {{
|
||||
showToast('❌ ' + (data.error || 'Fehler'), true);
|
||||
}}
|
||||
}} catch(e) {{
|
||||
if (btn) btn.classList.remove('uploading');
|
||||
showToast('❌ ' + e.message, true);
|
||||
}}
|
||||
}}
|
||||
|
||||
const uploadZone = document.getElementById('uploadZone');
|
||||
uploadZone.addEventListener('dragover', e => {{ e.preventDefault(); uploadZone.classList.add('dragover'); }});
|
||||
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
|
||||
@@ -797,7 +849,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
||||
filepath = os.path.join(month_dir, unique_name)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(data)
|
||||
f.write(file_data)
|
||||
|
||||
# ── Article-Image Association ──────────────────────────────────────
|
||||
association_msg = ""
|
||||
|
||||
Reference in New Issue
Block a user