Files
pokedexter/backend/app/templates/admin/cards.html
T
glennigen 54465f751c Admin: serie-detail, billedhåndtering, dark mode fix, source_url kolonne + verify
- Serie-detail side: sæt under serie + enkelt/bulk opret
- Billedhentning: URL→lokal download med source_url til re-sync
- Advarselsbadge (⚠) på kort med eksterne URLs (ingen streaming)
- Sync/Verify endpoints + knapper i admin UI
- Brugerstyring: opret/slet (self-delete beskyttet)
- Dark mode flash fix: render-blokerende script i <head>
- Persistent volume til billeder (pokedexter_images)
2026-07-01 23:11:55 +02:00

179 lines
7.3 KiB
HTML

{% extends "base.html" %}
{% block title %}Admin — {{ set.code }} kort{% endblock %}
{% block content %}
<a href="/admin/sets" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">&larr; Sæt</a>
<h1 style="margin-top:12px;">
<span class="tag">{{ set.code }}</span>
{{ set.name }}
</h1>
<p style="color:var(--text-muted);margin-bottom:16px;">{{ set.serie.name }}</p>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin:20px 0;">
<div class="card" style="flex:1;min-width:280px;">
<h3>Opret kort</h3>
<form onsubmit="createCard(event)" style="display:flex;flex-direction:column;gap:10px;margin-top:12px;">
<input type="text" id="card-number" placeholder="Nummer (f.eks. 050)" required>
<input type="text" id="card-name" placeholder="Navn (f.eks. Raichu)" required>
<input type="text" id="card-image" placeholder="Billede URL (valgfri)">
<button class="btn btn-primary">Opret</button>
</form>
</div>
<div class="card" style="flex:1;min-width:280px;">
<h3>CSV-import</h3>
<p style="font-size:0.85rem;color:var(--text-muted);margin:8px 0;">
Kolonner: <code>set_code,number,name,image</code>
<br><a href="/api/admin/templates/cards.csv" style="color:var(--accent);">Download skabelon</a>
</p>
<form onsubmit="importCards(event)" style="display:flex;flex-direction:column;gap:10px;">
<input type="file" id="cards-csv" accept=".csv" required>
<button class="btn btn-primary">Importer</button>
</form>
<div id="cards-import-result" style="margin-top:8px;"></div>
</div>
<div class="card" style="flex:1;min-width:280px;">
<h3>Synkronisér billeder</h3>
<p style="font-size:0.85rem;color:var(--text-muted);margin:8px 0;">
Hent alle eksterne billeder for {{ set.code }} og gem lokalt.
</p>
<button class="btn btn-primary" onclick="syncImages('{{ set.code }}')">📥 Synkronisér</button>
<div id="sync-result" style="margin-top:8px;"></div>
</div>
</div>
<h2>Kort i {{ set.code }}</h2>
<table>
<thead>
<tr><th>#</th><th>Navn</th><th>Kode</th><th>Billede</th><th>Upload</th><th>Handling</th></tr>
</thead>
<tbody>
{% for card in set.cards %}
<tr id="card-{{ card.id }}">
<td style="font-weight:600;">{{ card.number }}</td>
<td><a href="/card/{{ set.code }}/{{ card.number }}" style="color:var(--text);">{{ card.name }}</a></td>
<td><span class="tag">{{ card.full_code }}</span></td>
<td>
{% if card.image_path and (card.image_path.startswith('http://') or card.image_path.startswith('https://')) %}
<span class="warning-badge" title="Billede ikke synkroniseret — eksternt URL: {{ card.image_path }}">⚠️</span>
<span style="color:var(--accent);font-size:0.75rem;cursor:help;"
onclick="alert('{{ card.image_path }}')">eksternt</span>
{% elif card.image_url %}
<a href="{{ card.image_url }}" target="_blank">
<img src="{{ card.image_url }}" alt="{{ card.name }}"
style="height:40px;border-radius:4px;">
</a>
{% else %}
<span style="color:var(--text-muted);font-size:0.8rem;"></span>
{% endif %}
</td>
<td>
<form onsubmit="uploadImage(event, {{ card.id }})" style="display:flex;gap:6px;align-items:center;">
<input type="file" accept="image/*" style="width:auto;font-size:0.8rem;padding:4px;">
<button class="btn btn-outline btn-sm"></button>
</form>
</td>
<td>
<button class="btn btn-danger btn-sm" onclick="deleteCard({{ card.id }})">Slet</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if set.cards|length == 0 %}
<div class="card" style="text-align:center;color:var(--text-muted);margin-top:16px;">
<p>Ingen kort i {{ set.code }} endnu.</p>
</div>
{% endif %}
<script>
async function createCard(e) {
e.preventDefault();
const number = document.getElementById('card-number').value.trim();
const name = document.getElementById('card-name').value.trim();
const image = document.getElementById('card-image').value.trim() || null;
if (!number || !name) return;
const res = await fetch('/api/admin/sets/{{ set.code }}/cards', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({number, name, image})
});
const data = await res.json();
if (data.ok) {
location.reload();
} else {
alert(data.detail || 'Fejl');
}
}
async function deleteCard(id) {
if (!confirm('Slet dette kort?')) return;
const res = await fetch('/api/admin/cards/' + id, {method: 'DELETE'});
const data = await res.json();
if (data.ok) {
document.getElementById('card-' + id).remove();
} else {
alert('Fejl ved sletning');
}
}
async function importCards(e) {
e.preventDefault();
const file = document.getElementById('cards-csv').files[0];
if (!file) return;
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/admin/cards/import', {method: 'POST', body: form});
const data = await res.json();
const el = document.getElementById('cards-import-result');
if (data.ok) {
el.innerHTML = '<p style="color:green;">✓ ' + data.created + ' kort oprettet</p>';
if (data.errors.length) {
el.innerHTML += '<p style="color:var(--accent);">' + data.errors.join('<br>') + '</p>';
}
setTimeout(() => location.reload(), 1500);
} else {
el.innerHTML = '<p style="color:var(--accent);">Fejl ved import</p>';
}
}
async function uploadImage(e, cardId) {
e.preventDefault();
const file = e.target.querySelector('input[type="file"]').files[0];
if (!file) return;
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/admin/cards/' + cardId + '/upload', {
method: 'POST',
body: form
});
const data = await res.json();
if (data.ok) {
location.reload();
} else {
alert(data.detail || 'Fejl ved upload');
}
}
async function syncImages(setCode) {
const btn = event.target;
btn.disabled = true;
btn.textContent = 'Synkroniserer...';
const res = await fetch('/api/admin/sets/' + setCode + '/sync-images', {method: 'POST'});
const data = await res.json();
const el = document.getElementById('sync-result');
if (data.ok) {
let msg = '\u2713 ' + data.downloaded + ' billed(er) downloadet';
if (data.skipped > 0) msg += ', ' + data.skipped + ' sprunget over (allerede lokale)';
if (data.errors.length) msg += '<br><span style="color:var(--accent);">' + data.errors.join('<br>') + '</span>';
el.innerHTML = '<p style="color:green;">' + msg + '</p>';
setTimeout(() => location.reload(), 1500);
} else {
el.innerHTML = '<p style="color:var(--accent);">Fejl: ' + (data.detail || 'Ukendt') + '</p>';
}
btn.disabled = false;
btn.textContent = '\ud83d\udce5 Synkronis\u00e9r';
}
</script>
{% endblock %}