Add account connections, guild wallets and admin fixes [skip ci]

This commit is contained in:
SCOPEDD committed 2026-09-30 12:25:29 -04:00
1 parent 7076e847ea
commit 004862ea88
38 files changed
+1696 -458

No files matched your search

+44 -21
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Search, Plus, Pencil, Trash2, CheckCircle2, XCircle, Award, Sparkles, Filter } from '@lucide/svelte';
import { Search, Plus, Pencil, Trash2, CheckCircle2, XCircle, Sparkles, Pickaxe, Swords, Blocks, Wheat, Users, Compass, Target } from '@lucide/svelte';
import Modal from '../components/Modal.svelte';
import { get, post, put, del } from '../lib/api';
import { toast, toastError } from '../lib/toast.svelte';
@@ -19,6 +19,19 @@
let deleteOpen = $state(false);
const categories = ['mining', 'combat', 'building', 'farming', 'social', 'exploration'];
const iconChoices = [
{ id: 'pickaxe', label: 'Mining', component: Pickaxe },
{ id: 'swords', label: 'Combat', component: Swords },
{ id: 'blocks', label: 'Building', component: Blocks },
{ id: 'wheat', label: 'Farming', component: Wheat },
{ id: 'users', label: 'Social', component: Users },
{ id: 'compass', label: 'Exploration', component: Compass },
{ id: 'target', label: 'General', component: Target },
];
function iconFor(q: Partial<Quest>) {
const icon = (q.icon || '').toLowerCase();
return iconChoices.find((i) => i.id === icon)?.component || iconChoices.find((i) => i.label.toLowerCase() === q.category)?.component || Target;
}
const statTypes = [
{ id: 'blocks_broken', label: 'Blocks Broken' },
{ id: 'mob_kills', label: 'Mob Kills' },
@@ -44,7 +57,7 @@
xp_reward: q.xp_reward || 0,
stat_type: q.stat_type || q.target_stat || 'blocks_broken',
target_count: q.target_count || 1,
icon: q.icon || '⛏️',
icon: q.icon || 'pickaxe',
active: q.active !== undefined ? q.active : (q.enabled !== undefined ? q.enabled : true),
}));
} catch (e) {
@@ -84,7 +97,7 @@
xp_reward: 150,
stat_type: 'blocks_broken',
target_count: 50,
icon: '⛏️',
icon: 'pickaxe',
active: true,
};
draftOpen = true;
@@ -96,8 +109,8 @@
}
async function save() {
if (!draft || !draft.title || !draft.description) {
toast('Title and description are required', 'error');
if (!draft || !draft.id?.trim() || !draft.title?.trim() || !draft.description?.trim() || !draft.stat_type?.trim() || Number(draft.target_count) < 1 || Number(draft.xp_reward) < 0) {
toast('Enter an ID, title, description, tracked stat, target and reward', 'error');
return;
}
saving = true;
@@ -205,9 +218,10 @@
</thead>
<tbody>
{#each filtered as q}
{@const Icon = iconFor(q)}
<tr>
<td class="icon-cell">
<span class="quest-icon">{q.icon || '🎯'}</span>
<span class="quest-icon" title={q.icon}><Icon size={21} /></span>
</td>
<td class="info-cell">
<strong>{q.title}</strong>
@@ -260,11 +274,11 @@
</div>
{#if draft}
<Modal bind:open={draftOpen} title={draft.id && quests.some((q) => q.id === draft?.id) ? 'Edit Quest' : 'New Quest'}>
<Modal bind:open={draftOpen} width={720} title={draft.id && quests.some((q) => q.id === draft?.id) ? 'Edit Quest' : 'New Quest'}>
<form onsubmit={(e) => { e.preventDefault(); save(); }} class="form">
<div class="field">
<label for="q-id">Quest Identifier</label>
<input id="q-id" type="text" bind:value={draft.id} required placeholder="e.g. daily_mine_iron" />
<input id="q-id" type="text" bind:value={draft.id} required disabled={quests.some((q) => q.id === draft?.id)} placeholder="e.g. daily_mine_iron" />
</div>
<div class="grid-2">
@@ -273,8 +287,10 @@
<input id="q-title" type="text" bind:value={draft.title} required placeholder="e.g. Iron Seeker" />
</div>
<div class="field">
<label for="q-icon">Icon (Emoji / Symbol)</label>
<input id="q-icon" type="text" bind:value={draft.icon} placeholder="⛏️" />
<label for="q-icon">Icon</label>
<select id="q-icon" bind:value={draft.icon}>
{#each iconChoices as icon}<option value={icon.id}>{icon.label}</option>{/each}
</select>
</div>
</div>
@@ -304,11 +320,9 @@
<div class="grid-3">
<div class="field">
<label for="q-stat">Tracked Stat</label>
<select id="q-stat" bind:value={draft.stat_type}>
{#each statTypes as st}
<option value={st.id}>{st.label}</option>
{/each}
</select>
<input id="q-stat" list="quest-stats" bind:value={draft.stat_type} required placeholder="blocks_broken or action:block_broken:DIAMOND_ORE" />
<datalist id="quest-stats">{#each statTypes as st}<option value={st.id}>{st.label}</option>{/each}</datalist>
<small>Use an action target such as <code>action:block_broken:DIAMOND_ORE</code> for a specific block.</small>
</div>
<div class="field">
<label for="q-target">Target Count</label>
@@ -316,7 +330,7 @@
</div>
<div class="field">
<label for="q-xp">XP Reward</label>
<input id="q-xp" type="number" min="10" step="10" bind:value={draft.xp_reward} required />
<input id="q-xp" type="number" min="0" step="1" bind:value={draft.xp_reward} required />
</div>
</div>
@@ -348,7 +362,7 @@
{/if}
<style>
.page { padding: 32px; max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
.page { padding: clamp(16px, 3vw, 32px); width: 100%; min-width: 0; max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
header { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 6px; }
header p { color: var(--muted); margin: 0; font-size: 0.95rem; }
@@ -364,21 +378,21 @@
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
.search-box { display: flex; align-items: center; gap: 10px; background: var(--bg-2); border: 1px solid var(--line); border-radius: 10px; padding: 8px 14px; flex: 1; min-width: 260px; max-width: 500px; }
.search-box input { background: transparent; border: none; outline: none; color: var(--text); width: 100%; font-size: 0.9rem; }
.filters { display: flex; align-items: center; gap: 12px; }
.filters { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-width: 0; }
.segmented { display: flex; background: var(--bg-2); border: 1px solid var(--line); border-radius: 10px; padding: 3px; gap: 2px; }
.segmented button { padding: 6px 14px; border-radius: 7px; border: none; background: transparent; color: var(--muted); font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.15s; }
.segmented button.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
select { background: var(--bg-2); border: 1px solid var(--line); border-radius: 10px; padding: 8px 14px; color: var(--text); font-size: 0.85rem; outline: none; }
.table-wrap { background: var(--bg-2); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
.table-wrap { width: 100%; min-width: 0; background: var(--bg-2); border: 1px solid var(--line); border-radius: 14px; overflow-x: auto; }
table { width: 100%; min-width: 1040px; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
th { padding: 12px 16px; background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--line); color: var(--muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }
td { padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,0.04); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,0.015); }
.icon-cell { width: 48px; text-align: center; }
.quest-icon { font-size: 1.6rem; display: inline-flex; align-items: center; justify-content: center; }
.quest-icon { display: inline-flex; align-items: center; justify-content: center; color: var(--accent); }
.info-cell strong { font-size: 0.95rem; display: block; margin-bottom: 2px; }
.info-cell p { margin: 0 0 4px; color: var(--muted); font-size: 0.82rem; }
@@ -410,4 +424,13 @@
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.empty { text-align: center; padding: 60px 20px; color: var(--muted); background: var(--bg-2); border-radius: 12px; border: 1px solid var(--line); }
@media (max-width: 720px) {
header { flex-wrap: wrap; }
.stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.search-box { min-width: 0; max-width: none; flex-basis: 100%; }
.filters, .segmented { width: 100%; }
.segmented button { flex: 1; padding-inline: 4px; }
.grid-2, .grid-3 { grid-template-columns: minmax(0, 1fr); }
.form .field { min-width: 0; }
}
</style>