Initial: Pokedexter TCG-Collector webapp
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — Kort i {{ set_code }}{% 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 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) {
|
||||
alert('Fejl ved sletning');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('create-card-form').addEventListener('submit', async (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');
|
||||
}
|
||||
} catch(e) {
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ Kunne ikke forbinde';
|
||||
}
|
||||
});
|
||||
|
||||
loadCards();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin panel{% endblock %}
|
||||
{% block content %}
|
||||
<h1>⚙️ Admin panel</h1>
|
||||
<p style="color:var(--text-muted);margin-bottom:24px;">Velkommen, {{ 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>
|
||||
</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>
|
||||
</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,95 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — Serier{% 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>
|
||||
|
||||
<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>
|
||||
|
||||
<h2>Eksisterende serier</h2>
|
||||
<div id="set-list"></div>
|
||||
|
||||
<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 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) {
|
||||
alert('Fejl ved sletning');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('create-set-form').addEventListener('submit', async (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');
|
||||
}
|
||||
} catch(e) {
|
||||
msg.style.color = 'var(--accent)';
|
||||
msg.textContent = '❌ Kunne ikke forbinde';
|
||||
}
|
||||
});
|
||||
|
||||
loadSets();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user