Fix: eagerload series.sets i admin og API (MissingGreenlet i async mode)
This commit is contained in:
@@ -1,110 +1,146 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — Kort i {{ set_code }}{% endblock %}
|
||||
{% block title %}Admin — {{ set.code }} kort{% endblock %}
|
||||
{% block content %}
|
||||
<h1>🃏 Kort i <span class="tag">{{ set_code }}</span></h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:16px;">Tilføj eller fjern kort fra dette set.</p>
|
||||
<a href="/admin/sets" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">← 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 class="card" style="margin-bottom:24px;">
|
||||
<h2>Tilføj nyt kort</h2>
|
||||
<form id="create-card-form" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px;">
|
||||
<input type="text" name="number" placeholder="Nummer (f.eks. 068)" required>
|
||||
<input type="text" name="name" placeholder="Navn (f.eks. Garchomp EX)" required>
|
||||
<input type="text" name="type" placeholder="Type (f.eks. Colorless)">
|
||||
<input type="text" name="rarity" placeholder="Sjældenhed (f.eks. Rare)">
|
||||
<input type="number" name="hp" placeholder="HP">
|
||||
<input type="text" name="stage" placeholder="Stage (f.eks. Basic)">
|
||||
<input type="text" name="image_url" placeholder="Billede URL" style="grid-column:span 2;">
|
||||
<textarea name="description" placeholder="Beskrivelse" rows="2" style="grid-column:span 2;"></textarea>
|
||||
<button type="submit" class="btn btn-primary" style="grid-column:span 2;">Tilføj kort</button>
|
||||
</form>
|
||||
<p id="card-msg" style="font-size:0.85rem;margin-top:8px;"></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>
|
||||
|
||||
<h2>Eksisterende kort</h2>
|
||||
<div id="card-list">Indlæser...</div>
|
||||
<a href="/admin/sets" class="btn btn-outline" style="margin-top:16px;">← Tilbage til serier</a>
|
||||
<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_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>
|
||||
const SET_CODE = '{{ set_code }}';
|
||||
|
||||
async function loadCards() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/sets/' + SET_CODE + '/cards');
|
||||
const cards = await res.json();
|
||||
const container = document.getElementById('card-list');
|
||||
|
||||
if (cards.length === 0) {
|
||||
container.innerHTML = '<div class="card"><p style="color:var(--text-muted);">Ingen kort i dette set endnu.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div style="overflow-x:auto;"><table><thead><tr><th>#</th><th>Kode</th><th>Navn</th><th>Type</th><th>Sjældenhed</th><th style="width:60px;"></th></tr></thead><tbody>';
|
||||
cards.forEach(c => {
|
||||
html += '<tr>' +
|
||||
'<td>' + c.number + '</td>' +
|
||||
'<td><span class="tag">' + c.full_code + '</span></td>' +
|
||||
'<td>' + c.name + '</td>' +
|
||||
'<td>' + (c.type || '—') + '</td>' +
|
||||
'<td>' + (c.rarity || '—') + '</td>' +
|
||||
'<td><button class="btn btn-danger btn-sm" onclick="deleteCard(' + c.id + ')">✕</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
} catch(e) {
|
||||
document.getElementById('card-list').innerHTML = '<p style="color:var(--accent);">Kunne ikke indlæse kort</p>';
|
||||
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;
|
||||
try {
|
||||
const res = await fetch('/api/admin/cards/' + id, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
loadCards();
|
||||
} else {
|
||||
alert('Kunne ikke slette');
|
||||
}
|
||||
} catch(e) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('create-card-form').addEventListener('submit', async (e) => {
|
||||
async function importCards(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const msg = document.getElementById('card-msg');
|
||||
try {
|
||||
const res = await fetch('/api/admin/sets/' + SET_CODE + '/cards', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
number: form.number.value,
|
||||
name: form.name.value,
|
||||
type: form.type.value || null,
|
||||
rarity: form.rarity.value || null,
|
||||
hp: form.hp.value ? parseInt(form.hp.value) : null,
|
||||
stage: form.stage.value || null,
|
||||
description: form.description.value || null,
|
||||
image_url: form.image_url.value || null,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
form.reset();
|
||||
msg.style.color = 'var(--text)';
|
||||
msg.textContent = '✅ Kort tilføjet';
|
||||
loadCards();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ ' + (err.detail || 'Fejl');
|
||||
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>';
|
||||
}
|
||||
} catch(e) {
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ Kunne ikke forbinde';
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
el.innerHTML = '<p style="color:var(--accent);">Fejl ved import</p>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadCards();
|
||||
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');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,95 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin panel{% endblock %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block content %}
|
||||
<h1>⚙️ Admin panel</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">Velkommen, {{ user.username }}!</p>
|
||||
<h1>🔧 Adminpanel</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">Velkommen, {{ request.state.user.username }}!</p>
|
||||
|
||||
<div class="card-grid">
|
||||
<a href="/admin/sets" style="text-decoration:none;color:var(--text);">
|
||||
<div class="card" style="text-align:center;padding:32px;">
|
||||
<div style="font-size:2rem;margin-bottom:8px;">📦</div>
|
||||
<h3>Administrér serier</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">Opret, redigér og slet serier/sets</p>
|
||||
<a href="/admin/series" class="card-set">
|
||||
<div class="card" style="text-align:center;">
|
||||
<p style="font-size:2rem;margin-bottom:8px;">📦</p>
|
||||
<h3>Serier</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">Administrér serier</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/search" style="text-decoration:none;color:var(--text);">
|
||||
<div class="card" style="text-align:center;padding:32px;">
|
||||
<div style="font-size:2rem;margin-bottom:8px;">🔍</div>
|
||||
<h3>Se kort</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">Søg og gennemse alle kort</p>
|
||||
<a href="/admin/sets" class="card-set">
|
||||
<div class="card" style="text-align:center;">
|
||||
<p style="font-size:2rem;margin-bottom:8px;">📁</p>
|
||||
<h3>Sæt</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">Administrér sæt</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/search" class="card-set">
|
||||
<div class="card" style="text-align:center;">
|
||||
<p style="font-size:2rem;margin-bottom:8px;">🔍</p>
|
||||
<h3>Søg</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">Søg efter kort</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:24px;">
|
||||
<h2>👥 Brugere</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Brugernavn</th><th>Rolle</th><th>Oprettet</th></tr>
|
||||
</thead>
|
||||
<tbody id="user-list">
|
||||
<tr><td colspan="3">Indlæser...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:16px;">
|
||||
<h3>Opret ny bruger</h3>
|
||||
<form id="create-user-form" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;">
|
||||
<input type="text" name="username" placeholder="Brugernavn" required style="max-width:200px;">
|
||||
<input type="password" name="password" placeholder="Adgangskode" required style="max-width:200px;">
|
||||
<select name="role" style="max-width:120px;">
|
||||
<option value="user">Bruger</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Opret</button>
|
||||
</form>
|
||||
<p id="user-msg" style="font-size:0.85rem;margin-top:8px;"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/users');
|
||||
const users = await res.json();
|
||||
const tbody = document.getElementById('user-list');
|
||||
tbody.innerHTML = users.map(u =>
|
||||
'<tr><td>' + u.username + '</td><td><span class="tag">' + u.role + '</span></td><td>' + (u.created_at || '') + '</td></tr>'
|
||||
).join('');
|
||||
} catch(e) {
|
||||
document.getElementById('user-list').innerHTML = '<tr><td colspan="3">Kunne ikke indlæse brugere</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('create-user-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const msg = document.getElementById('user-msg');
|
||||
try {
|
||||
const res = await fetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: form.username.value,
|
||||
password: form.password.value,
|
||||
role: form.role.value,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
form.reset();
|
||||
msg.style.color = 'var(--text)';
|
||||
msg.textContent = '✅ Bruger oprettet';
|
||||
loadUsers();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ ' + (err.detail || 'Fejl');
|
||||
}
|
||||
} catch(e) {
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ Kunne ikke forbinde';
|
||||
}
|
||||
});
|
||||
|
||||
loadUsers();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — Serier{% endblock %}
|
||||
{% block content %}
|
||||
<a href="/admin" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">← Admin</a>
|
||||
<h1 style="margin-top:12px;">📦 Serier</h1>
|
||||
|
||||
<div style="display:flex;gap:24px;flex-wrap:wrap;margin:20px 0;">
|
||||
<div class="card" style="flex:1;min-width:280px;">
|
||||
<h3>Opret serie</h3>
|
||||
<form onsubmit="createSerie(event)" style="display:flex;flex-direction:column;gap:10px;margin-top:12px;">
|
||||
<input type="text" id="serie-name" placeholder="Navn (f.eks. Sword & Shield)" required>
|
||||
<input type="text" id="serie-desc" placeholder="Beskrivelse (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>name,description</code>
|
||||
<br><a href="/api/admin/templates/series.csv" style="color:var(--accent);">Download skabelon</a>
|
||||
</p>
|
||||
<form onsubmit="importSeries(event)" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<input type="file" id="series-csv" accept=".csv" required>
|
||||
<button class="btn btn-primary">Importer</button>
|
||||
</form>
|
||||
<div id="series-import-result" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Eksisterende serier</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Navn</th><th>Sæt</th><th>Handling</th></tr>
|
||||
</thead>
|
||||
<tbody id="series-list">
|
||||
{% for s in series %}
|
||||
<tr id="serie-{{ s.id }}">
|
||||
<td><strong>{{ s.name }}</strong></td>
|
||||
<td>{{ s.sets|length }}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteSerie({{ s.id }})">Slet</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if series|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);margin-top:16px;">
|
||||
<p>Ingen serier endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
async function createSerie(e) {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('serie-name').value.trim();
|
||||
const desc = document.getElementById('serie-desc').value.trim();
|
||||
if (!name) return;
|
||||
const res = await fetch('/api/admin/series', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name, description: desc || null})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.detail || 'Fejl');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSerie(id) {
|
||||
if (!confirm('Slet denne serie og alle dens sæt og kort?')) return;
|
||||
const res = await fetch('/api/admin/series/' + id, {method: 'DELETE'});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
document.getElementById('serie-' + id).remove();
|
||||
} else {
|
||||
alert('Fejl ved sletning');
|
||||
}
|
||||
}
|
||||
|
||||
async function importSeries(e) {
|
||||
e.preventDefault();
|
||||
const file = document.getElementById('series-csv').files[0];
|
||||
if (!file) return;
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const res = await fetch('/api/admin/series/import', {method: 'POST', body: form});
|
||||
const data = await res.json();
|
||||
const el = document.getElementById('series-import-result');
|
||||
if (data.ok) {
|
||||
el.innerHTML = '<p style="color:green;">✓ ' + data.created + ' serie(r) 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>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,95 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — Serier{% endblock %}
|
||||
{% block title %}Admin — Sæt{% endblock %}
|
||||
{% block content %}
|
||||
<h1>📦 Administrér serier</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:16px;">Opret og slet Pokémon TCG serier/sets.</p>
|
||||
<a href="/admin" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">← Admin</a>
|
||||
<h1 style="margin-top:12px;">📁 Sæt</h1>
|
||||
|
||||
<div class="card" style="margin-bottom:24px;">
|
||||
<h2>Opret ny serie</h2>
|
||||
<form id="create-set-form" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;">
|
||||
<input type="text" name="code" placeholder="Kode (f.eks. CRI)" required style="max-width:120px;text-transform:uppercase;">
|
||||
<input type="text" name="name" placeholder="Navn (f.eks. Chaos Rising)" required style="max-width:280px;">
|
||||
<input type="text" name="release_date" placeholder="Udgivelsesdato" style="max-width:160px;">
|
||||
<button type="submit" class="btn btn-primary">Opret</button>
|
||||
</form>
|
||||
<p id="set-msg" style="font-size:0.85rem;margin-top:8px;"></p>
|
||||
<div style="display:flex;gap:24px;flex-wrap:wrap;margin:20px 0;">
|
||||
<div class="card" style="flex:1;min-width:280px;">
|
||||
<h3>Opret sæt</h3>
|
||||
<form onsubmit="createSet(event)" style="display:flex;flex-direction:column;gap:10px;margin-top:12px;">
|
||||
<select id="set-serie" required>
|
||||
<option value="">— Vælg serie —</option>
|
||||
{% for s in series %}
|
||||
<option value="{{ s.name }}">{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" id="set-code" placeholder="Kode (f.eks. EVS)" required>
|
||||
<input type="text" id="set-name" placeholder="Navn (f.eks. Evolving Skies)" required>
|
||||
<input type="date" id="set-date" placeholder="Udgivelsesdato">
|
||||
<input type="text" id="set-desc" placeholder="Beskrivelse (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>serie,code,name,release_date,description</code>
|
||||
<br><a href="/api/admin/templates/sets.csv" style="color:var(--accent);">Download skabelon</a>
|
||||
</p>
|
||||
<form onsubmit="importSets(event)" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<input type="file" id="sets-csv" accept=".csv" required>
|
||||
<button class="btn btn-primary">Importer</button>
|
||||
</form>
|
||||
<div id="sets-import-result" style="margin-top:8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Eksisterende serier</h2>
|
||||
<div id="set-list"></div>
|
||||
<h2>Eksisterende sæt</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Kode</th><th>Navn</th><th>Serie</th><th>Kort</th><th>Handling</th></tr>
|
||||
</thead>
|
||||
<tbody id="sets-list">
|
||||
{% for s in sets %}
|
||||
<tr id="set-{{ s.id }}">
|
||||
<td><span class="tag">{{ s.code }}</span></td>
|
||||
<td><a href="/admin/cards/{{ s.code }}" style="color:var(--text);">{{ s.name }}</a></td>
|
||||
<td>{{ s.serie.name }}</td>
|
||||
<td>{{ s.cards|length }}</td>
|
||||
<td>
|
||||
<a href="/admin/cards/{{ s.code }}" class="btn btn-outline btn-sm">Kort</a>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteSet({{ s.id }})">Slet</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if sets|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);margin-top:16px;">
|
||||
<p>Ingen sæt endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
async function loadSets() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/sets');
|
||||
const sets = await res.json();
|
||||
const container = document.getElementById('set-list');
|
||||
|
||||
if (sets.length === 0) {
|
||||
container.innerHTML = '<div class="card"><p style="color:var(--text-muted);">Ingen serier endnu.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div style="overflow-x:auto;"><table><thead><tr><th>Kode</th><th>Navn</th><th>Udgivet</th><th style="width:100px;"></th></tr></thead><tbody>';
|
||||
sets.forEach(s => {
|
||||
html += '<tr>' +
|
||||
'<td><span class="tag">' + s.code + '</span></td>' +
|
||||
'<td><a href="/admin/cards/' + s.code + '" style="color:var(--text);">' + s.name + '</a></td>' +
|
||||
'<td>' + (s.release_date || '—') + '</td>' +
|
||||
'<td><button class="btn btn-danger btn-sm" onclick="deleteSet(' + s.id + ', \'' + s.code + '\')">Slet</button></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
} catch(e) {
|
||||
document.getElementById('set-list').innerHTML = '<p style="color:var(--accent);">Kunne ikke indlæse serier</p>';
|
||||
async function createSet(e) {
|
||||
e.preventDefault();
|
||||
const serie = document.getElementById('set-serie').value;
|
||||
const code = document.getElementById('set-code').value.trim().toUpperCase();
|
||||
const name = document.getElementById('set-name').value.trim();
|
||||
const date = document.getElementById('set-date').value || null;
|
||||
const desc = document.getElementById('set-desc').value.trim() || null;
|
||||
if (!serie || !code || !name) return;
|
||||
const res = await fetch('/api/admin/series/' + encodeURIComponent(serie) + '/sets', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({code, name, release_date: date, description: desc})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.detail || 'Fejl');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSet(id, code) {
|
||||
if (!confirm('Slet serien ' + code + '? Alle kort i serien slettes også.')) return;
|
||||
try {
|
||||
const res = await fetch('/api/admin/sets/' + id, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
loadSets();
|
||||
} else {
|
||||
alert('Kunne ikke slette');
|
||||
}
|
||||
} catch(e) {
|
||||
async function deleteSet(id) {
|
||||
if (!confirm('Slet dette sæt og alle dets kort?')) return;
|
||||
const res = await fetch('/api/admin/sets/' + id, {method: 'DELETE'});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
document.getElementById('set-' + id).remove();
|
||||
} else {
|
||||
alert('Fejl ved sletning');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('create-set-form').addEventListener('submit', async (e) => {
|
||||
async function importSets(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const msg = document.getElementById('set-msg');
|
||||
try {
|
||||
const res = await fetch('/api/admin/sets', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
code: form.code.value,
|
||||
name: form.name.value,
|
||||
release_date: form.release_date.value || null,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
form.reset();
|
||||
msg.style.color = 'var(--text)';
|
||||
msg.textContent = '✅ Serie oprettet';
|
||||
loadSets();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ ' + (err.detail || 'Fejl');
|
||||
const file = document.getElementById('sets-csv').files[0];
|
||||
if (!file) return;
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const res = await fetch('/api/admin/sets/import', {method: 'POST', body: form});
|
||||
const data = await res.json();
|
||||
const el = document.getElementById('sets-import-result');
|
||||
if (data.ok) {
|
||||
el.innerHTML = '<p style="color:green;">✓ ' + data.created + ' sæt oprettet</p>';
|
||||
if (data.errors.length) {
|
||||
el.innerHTML += '<p style="color:var(--accent);">' + data.errors.join('<br>') + '</p>';
|
||||
}
|
||||
} catch(e) {
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ Kunne ikke forbinde';
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
el.innerHTML = '<p style="color:var(--accent);">Fejl ved import</p>';
|
||||
}
|
||||
});
|
||||
|
||||
loadSets();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -239,7 +239,8 @@
|
||||
<nav>
|
||||
<a href="/" class="logo">⚡ Pokedexter</a>
|
||||
<a href="/">Hjem</a>
|
||||
<a href="/sets">Serier</a>
|
||||
<a href="/series">Serier</a>
|
||||
<a href="/sets">Sæt</a>
|
||||
<a href="/search">Søg</a>
|
||||
{% if request.state.user and request.state.user.role == "admin" %}
|
||||
<a href="/admin">Admin</a>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ card.full_code }} — {{ card.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<a href="/sets/{{ card.set.code }}" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">
|
||||
← {{ card.set.serie.name }} — {{ card.set.name }}
|
||||
</a>
|
||||
|
||||
<div style="display:flex;gap:32px;margin-top:20px;flex-wrap:wrap;">
|
||||
<div style="flex:0 0 auto;max-width:400px;">
|
||||
{% if card.image_url %}
|
||||
<img src="{{ card.image_url }}" alt="{{ card.name }}"
|
||||
style="width:100%;border-radius:12px;border:2px solid var(--border);box-shadow:0 4px 20px var(--shadow);">
|
||||
{% else %}
|
||||
<div class="card" style="width:320px;height:440px;display:flex;align-items:center;justify-content:center;
|
||||
text-align:center;color:var(--text-muted);">
|
||||
<div>
|
||||
<p style="font-size:3rem;margin-bottom:12px;">🃏</p>
|
||||
<p>Intet billede tilgængeligt</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="flex:1;min-width:200px;">
|
||||
<h1 style="margin-bottom:4px;">
|
||||
<span class="tag">{{ card.full_code }}</span>
|
||||
</h1>
|
||||
<h2 style="font-size:1.8rem;margin:0 0 20px 0;color:var(--text);">{{ card.name }}</h2>
|
||||
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<table style="margin:0;">
|
||||
<tr>
|
||||
<td style="font-weight:600;width:100px;">Sæt</td>
|
||||
<td>
|
||||
<a href="/sets/{{ card.set.code }}" style="color:var(--text);">
|
||||
{{ card.set.code }} — {{ card.set.name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:600;">Serie</td>
|
||||
<td>
|
||||
<a href="/series/{{ card.set.serie.name|urlencode }}" style="color:var(--text);">
|
||||
{{ card.set.serie.name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:600;">Nummer</td>
|
||||
<td>{{ card.number }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,27 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Forside{% endblock %}
|
||||
{% block content %}
|
||||
<h1>📦 Alle serier</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">{{ sets|length }} serie(r) i databasen</p>
|
||||
<h1>⚡ Pokémon TCG serier</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">{{ series|length }} serie(r) i databasen</p>
|
||||
|
||||
<div class="card-grid">
|
||||
{% for s in sets %}
|
||||
<a href="/sets/{{ s.code }}" class="card-set">
|
||||
{% for serie in series %}
|
||||
<a href="/series/{{ serie.name|urlencode }}" class="card-set">
|
||||
<div class="card">
|
||||
<span class="tag">{{ s.code }}</span>
|
||||
<h3 style="margin-top:10px;">{{ s.name }}</h3>
|
||||
{% if s.release_date %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:4px;">📅 {{ s.release_date }}</p>
|
||||
<h3 style="margin-bottom:6px;">📦 {{ serie.name }}</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">
|
||||
{{ serie.sets|length }} sæt
|
||||
</p>
|
||||
{% if serie.description %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:6px;">{{ serie.description }}</p>
|
||||
{% endif %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:4px;">{{ s.cards|length }} kort</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if sets|length == 0 %}
|
||||
{% if series|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);">
|
||||
<p>Ingen serier endnu. Log ind som admin for at tilføje.</p>
|
||||
<p>Ingen serier i databasen endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -24,21 +24,20 @@
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const data = { username: form.username.value, password: form.password.value };
|
||||
const formData = { username: form.username.value, password: form.password.value };
|
||||
const errEl = document.getElementById('login-error');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
redirect: 'manual',
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
if (res.ok) {
|
||||
window.location.href = '/';
|
||||
const json = await res.json();
|
||||
if (json.ok) {
|
||||
window.location.href = json.redirect || '/';
|
||||
} else {
|
||||
const err = await res.json();
|
||||
errEl.textContent = err.detail || 'Login fejlede';
|
||||
errEl.textContent = json.detail || 'Login fejlede';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
} catch(e) {
|
||||
|
||||
@@ -2,49 +2,65 @@
|
||||
{% block title %}Søg{% endblock %}
|
||||
{% block content %}
|
||||
<h1>🔍 Søg efter kort</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:16px;">Søg på kortkode (f.eks. <strong>CRI-068</strong>) eller kortnavn.</p>
|
||||
<p style="color:var(--text-muted);margin-bottom:16px;">
|
||||
Søg på kode (f.eks. <strong>CRI-068</strong>) eller kortnavn (f.eks. <strong>Pikachu</strong>)
|
||||
</p>
|
||||
|
||||
<div class="search-box">
|
||||
<input type="text" id="search-input" placeholder="CRI-068 eller kortnavn..." autofocus>
|
||||
<button class="btn btn-primary" onclick="doSearch()">Søg</button>
|
||||
<input type="text" id="search-input" placeholder="Søg på kortkode eller navn..."
|
||||
onkeydown="if(event.key==='Enter') search()">
|
||||
<button class="btn btn-primary" onclick="search()">Søg</button>
|
||||
</div>
|
||||
|
||||
<div id="search-results"></div>
|
||||
<div id="search-results">
|
||||
<p style="color:var(--text-muted);text-align:center;padding:40px 0;">
|
||||
Indtast en søgning for at finde kort.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const input = document.getElementById('search-input');
|
||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); });
|
||||
async function search() {
|
||||
const q = document.getElementById('search-input').value.trim();
|
||||
const el = document.getElementById('search-results');
|
||||
|
||||
async function doSearch() {
|
||||
const q = input.value.trim();
|
||||
const container = document.getElementById('search-results');
|
||||
if (!q) { container.innerHTML = ''; return; }
|
||||
if (!q) {
|
||||
el.innerHTML = '<p style="color:var(--text-muted);text-align:center;padding:40px 0;">Indtast en søgning.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<p style="color:var(--text-muted);">Søger...</p>';
|
||||
el.innerHTML = '<p style="color:var(--text-muted);text-align:center;padding:20px 0;">Søger...</p>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/search?q=' + encodeURIComponent(q));
|
||||
const data = await res.json();
|
||||
container.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
container.innerHTML = '<div class="card"><p style="color:var(--text-muted);">Ingen kort fundet for "<strong>' + q + '</strong>"</p></div>';
|
||||
el.innerHTML = '<p style="color:var(--text-muted);text-align:center;padding:40px 0;">Ingen resultater.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="card" style="padding:0;"><div style="padding:10px 14px;border-bottom:1px solid var(--border);font-size:0.85rem;color:var(--text-muted);">' + data.length + ' resultat(er)</div>';
|
||||
let html = '<p style="margin-bottom:12px;">' + data.length + ' resultat(er)</p>';
|
||||
data.forEach(c => {
|
||||
const imgHtml = c.image_url
|
||||
? '<img src="' + c.image_url + '" alt="' + c.name + '" style="height:50px;border-radius:4px;">'
|
||||
: '<span style="color:var(--text-muted);font-size:0.8rem;">Intet billede</span>';
|
||||
html += '<div class="result-row">' +
|
||||
'<div><span class="tag">' + c.full_code + '</span> <strong>' + c.name + '</strong>' +
|
||||
' <span style="color:var(--text-muted);font-size:0.85rem;">' + c.set_name + '</span></div>' +
|
||||
'<div style="font-size:0.85rem;color:var(--text-muted);">' + c.type + ' · ' + c.rarity + '</div>' +
|
||||
'</div>';
|
||||
'<div>' +
|
||||
'<strong>' + c.full_code + '</strong> ' + c.name +
|
||||
'<br><span style="color:var(--text-muted);font-size:0.8rem;">' +
|
||||
c.serie_name + ' · ' + c.set_name +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div style="display:flex;align-items:center;gap:10px;">' +
|
||||
imgHtml +
|
||||
'<a href="/card/' + c.set_code + '/' + c.full_code.split('-')[1] + '" class="btn btn-outline btn-sm">Se kort</a>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
el.innerHTML = html;
|
||||
} catch(e) {
|
||||
container.innerHTML = '<p style="color:var(--accent);">Fejl: kunne ikke søge</p>';
|
||||
el.innerHTML = '<p style="color:var(--accent);text-align:center;">Fejl ved søgning.</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ serie.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<a href="/series" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">← Tilbage til serier</a>
|
||||
|
||||
<h1 style="margin-top:12px;">📦 {{ serie.name }}</h1>
|
||||
{% if serie.description %}
|
||||
<p style="color:var(--text-muted);margin-bottom:16px;">{{ serie.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:20px;">{{ serie.sets|length }} sæt</p>
|
||||
|
||||
<div class="card-grid">
|
||||
{% for s in serie.sets %}
|
||||
<a href="/sets/{{ s.code }}" class="card-set">
|
||||
<div class="card">
|
||||
<h3 style="margin-bottom:6px;">
|
||||
<span class="tag">{{ s.code }}</span>
|
||||
{{ s.name }}
|
||||
</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">
|
||||
{{ s.cards|length }} kort
|
||||
</p>
|
||||
{% if s.release_date %}
|
||||
<p style="font-size:0.8rem;color:var(--text-muted);margin-top:4px;">
|
||||
Udgivet: {{ s.release_date }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if serie.sets|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);">
|
||||
<p>Ingen sæt i denne serie endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Serier{% endblock %}
|
||||
{% block content %}
|
||||
<h1>📦 Pokémon TCG serier</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">{{ series|length }} serie(r)</p>
|
||||
|
||||
<div class="card-grid">
|
||||
{% for serie in series %}
|
||||
<a href="/series/{{ serie.name|urlencode }}" class="card-set">
|
||||
<div class="card">
|
||||
<h3 style="margin-bottom:6px;">{{ serie.name }}</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">
|
||||
{{ serie.sets|length }} sæt ·
|
||||
{% set ns = namespace(total=0) %}
|
||||
{% for s in serie.sets %}{% set ns.total = ns.total + s.cards|length %}{% endfor %}
|
||||
{{ ns.total }} kort
|
||||
</p>
|
||||
{% if serie.description %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:6px;">{{ serie.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if series|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);">
|
||||
<p>Ingen serier endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,54 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ set.code }} — {{ set.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div style="margin-bottom:20px;">
|
||||
<a href="/sets" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">← Tilbage til serier</a>
|
||||
</div>
|
||||
<a href="/series/{{ set.serie.name|urlencode }}" style="color:var(--text-muted);text-decoration:none;font-size:0.9rem;">
|
||||
← {{ set.serie.name }}
|
||||
</a>
|
||||
|
||||
<div class="card" style="margin-bottom:24px;">
|
||||
<span class="tag" style="font-size:1rem;">{{ set.code }}</span>
|
||||
<h1 style="margin-top:8px;">{{ set.name }}</h1>
|
||||
{% if set.release_date %}
|
||||
<p style="color:var(--text-muted);">📅 Udgivet: {{ set.release_date }}</p>
|
||||
{% endif %}
|
||||
{% if set.description %}
|
||||
<p style="margin-top:8px;">{{ set.description }}</p>
|
||||
{% endif %}
|
||||
<p style="font-size:0.9rem;color:var(--text-muted);margin-top:8px;">{{ set.cards|length }} kort i dette set</p>
|
||||
</div>
|
||||
<h1 style="margin-top:12px;">
|
||||
<span class="tag">{{ set.code }}</span>
|
||||
{{ set.name }}
|
||||
</h1>
|
||||
|
||||
<h2>Kort i {{ set.code }}</h2>
|
||||
{% if set.release_date %}
|
||||
<p style="color:var(--text-muted);margin-bottom:6px;">Udgivet: {{ set.release_date }}</p>
|
||||
{% endif %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:20px;">{{ set.cards|length }} kort</p>
|
||||
|
||||
{% if set.cards|length > 0 %}
|
||||
<div style="overflow-x:auto;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Kode</th>
|
||||
<th>Navn</th>
|
||||
<th>Type</th>
|
||||
<th>Sjældenhed</th>
|
||||
{% if set.cards[0].hp %}<th>HP</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for card in set.cards %}
|
||||
<tr>
|
||||
<td>{{ card.number }}</td>
|
||||
<td><span class="tag">{{ card.full_code }}</span></td>
|
||||
<td>{{ card.name }}</td>
|
||||
<td>{{ card.type or "—" }}</td>
|
||||
<td>{{ card.rarity or "—" }}</td>
|
||||
{% if card.hp %}<td>{{ card.hp }}</td>{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Navn</th>
|
||||
<th>Kode</th>
|
||||
<th>Billede</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for card in set.cards %}
|
||||
<tr>
|
||||
<td style="font-weight:600;">{{ card.number }}</td>
|
||||
<td>
|
||||
<a href="/card/{{ set.code }}/{{ card.number }}" style="color:var(--text);text-decoration:none;">
|
||||
{{ card.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td><span class="tag">{{ card.full_code }}</span></td>
|
||||
<td>
|
||||
{% if card.image_url %}
|
||||
<a href="/card/{{ set.code }}/{{ card.number }}">
|
||||
<img src="{{ card.image_url }}" alt="{{ card.name }}"
|
||||
style="height:60px;border-radius:6px;object-fit:contain;">
|
||||
</a>
|
||||
{% else %}
|
||||
<span style="color:var(--text-muted);font-size:0.8rem;">Intet billede</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if set.cards|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);">
|
||||
<p>Ingen kort i dette set endnu.</p>
|
||||
<p>Ingen kort i dette sæt endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -1,19 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Alle serier{% endblock %}
|
||||
{% block title %}Alle sæt{% endblock %}
|
||||
{% block content %}
|
||||
<h1>📦 Pokémon TCG serier</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">{{ sets|length }} serie(r) i databasen</p>
|
||||
<h1>📦 Alle sæt</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">{{ sets|length }} sæt i databasen</p>
|
||||
|
||||
<div class="card-grid">
|
||||
{% for s in sets %}
|
||||
<a href="/sets/{{ s.code }}" class="card-set">
|
||||
<div class="card">
|
||||
<span class="tag">{{ s.code }}</span>
|
||||
<h3 style="margin-top:10px;">{{ s.name }}</h3>
|
||||
<h3 style="margin-bottom:6px;">
|
||||
<span class="tag">{{ s.code }}</span>
|
||||
{{ s.name }}
|
||||
</h3>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);">
|
||||
{{ s.serie.name }} · {{ s.cards|length }} kort
|
||||
</p>
|
||||
{% if s.release_date %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:4px;">📅 {{ s.release_date }}</p>
|
||||
<p style="font-size:0.8rem;color:var(--text-muted);margin-top:4px;">
|
||||
Udgivet: {{ s.release_date }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p style="font-size:0.85rem;color:var(--text-muted);margin-top:4px;">{{ s.cards|length }} kort</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -21,7 +27,7 @@
|
||||
|
||||
{% if sets|length == 0 %}
|
||||
<div class="card" style="text-align:center;color:var(--text-muted);">
|
||||
<p>Ingen serier i databasen endnu.</p>
|
||||
<p>Ingen sæt endnu.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user