98 lines
7.4 KiB
Svelte
98 lines
7.4 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from 'svelte';
|
||
import { page } from '$app/stores';
|
||
import { goto } from '$app/navigation';
|
||
import { confirmAction } from '$ui/confirm.js';
|
||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||
import Button from '$ui/components/primitives/Button.svelte';
|
||
import ListFooter from '$ui/components/primitives/ListFooter.svelte';
|
||
import BlueprintBuilder from '$ui/components/BlueprintBuilder.svelte';
|
||
import DeployCollectionDialog from '$ui/components/DeployCollectionDialog.svelte';
|
||
import type { BlueprintSummary } from '$lib/deployment-plan.js';
|
||
let blueprintId = $state('');
|
||
let builderOpen = $state(false);
|
||
let editingBlueprint = $state<BlueprintSummary | null>(null);
|
||
let collectionOpen = $state(false);
|
||
let blueprints = $state<BlueprintSummary[]>([]);
|
||
let blueprintsLoading = $state(true);
|
||
let blueprintError = $state('');
|
||
let removing = $state('');
|
||
let limit = $state(25);
|
||
$effect(() => { limit = Math.max(25, Number($page.url.searchParams.get('limit')) || 25); });
|
||
function saveContext() {
|
||
const url = new URL($page.url);
|
||
if (limit > 25) url.searchParams.set('limit', String(limit)); else url.searchParams.delete('limit');
|
||
void goto(url, { replaceState: true, noScroll: true, keepFocus: true });
|
||
}
|
||
onMount(() => { void loadBlueprints(); });
|
||
function blueprintSaved(blueprint: BlueprintSummary, launch: boolean) {
|
||
blueprints = [...blueprints.filter(item => item.id !== blueprint.id), blueprint].sort((a, b) => a.name.localeCompare(b.name));
|
||
if (launch) { blueprintId = blueprint.id; collectionOpen = true; }
|
||
}
|
||
async function loadBlueprints() {
|
||
try {
|
||
const res = await fetch('/api/blueprints');
|
||
if (!res.ok) throw new Error('Could not load your collections.');
|
||
blueprints = (await res.json()).blueprints;
|
||
blueprintError = '';
|
||
} catch (err) { blueprintError = (err as Error).message; }
|
||
finally { blueprintsLoading = false; }
|
||
}
|
||
async function removeBlueprint(id: string) {
|
||
if (!await confirmAction({title:'Remove collection?', message:'Existing servers and launch history will be kept.', confirmLabel:'Remove collection', danger:true})) return;
|
||
removing = id; blueprintError = '';
|
||
try {
|
||
const res = await fetch('/api/blueprints/' + encodeURIComponent(id), { method: 'DELETE' });
|
||
if (!res.ok) throw new Error('Could not remove collection.');
|
||
blueprints = blueprints.filter(b => b.id !== id);
|
||
} catch (err) { blueprintError = (err as Error).message; }
|
||
finally { removing = ''; }
|
||
}
|
||
</script>
|
||
<svelte:head><title>Collections | Electric Cherry</title></svelte:head>
|
||
<div class="page">
|
||
<SectionHeader title="Collections" subtitle="Save your lineup of maps and lobby counts, then preview changes to your fleet." />
|
||
<BlueprintBuilder bind:open={builderOpen} blueprint={editingBlueprint} onsaved={blueprintSaved} />
|
||
<DeployCollectionDialog bind:open={collectionOpen} {blueprintId} />
|
||
<div class="ui-actions"><Button variant="primary" onclick={() => { editingBlueprint = null; builderOpen = true; }}>Create collection</Button></div>
|
||
<section>
|
||
<h2>Your collections <span>{blueprints.length}</span></h2>
|
||
{#if blueprintError}<p role="alert" class="error">{blueprintError} <button onclick={loadBlueprints}>Retry</button></p>{/if}
|
||
{#if blueprintsLoading}<p>Loading collections…</p>
|
||
{:else if !blueprints.length}<div class="blueprint-empty"><strong>Plan the whole lineup.</strong><p>Add maps from either game, choose the total lobbies each needs, and fill any gaps from one collection.</p><button onclick={() => { editingBlueprint = null; builderOpen = true; }}>Create your first collection →</button></div>
|
||
{:else}
|
||
<div class="blueprints">
|
||
{#each blueprints.slice(0,limit) as blueprint (blueprint.id)}
|
||
<article class="blueprint">
|
||
<div class="blueprint-top">
|
||
<span class="eyebrow">Server collection</span>
|
||
<span class="file-count">{blueprint.totalServers} target server{blueprint.totalServers === 1 ? '' : 's'}</span>
|
||
</div>
|
||
<h3>{blueprint.name}</h3>
|
||
<p>{blueprint.description || 'Fill missing lobbies or match your fleet to this lineup exactly.'}</p>
|
||
<ul class="blueprint-lineup">{#each blueprint.entries as entry}<li><strong>{entry.count}×</strong> {entry.mapName} <small>{entry.game.toUpperCase()}</small></li>{/each}</ul>
|
||
<div class="blueprint-footer">
|
||
<Button onclick={() => { blueprintId = blueprint.id; collectionOpen = true; }}>Preview fleet changes →</Button>
|
||
<button class="edit" onclick={() => { editingBlueprint = blueprint; builderOpen = true; }}>Edit</button>
|
||
<button class="remove" disabled={!!removing} onclick={() => removeBlueprint(blueprint.id)}>Remove</button>
|
||
</div>
|
||
</article>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</section>
|
||
<ListFooter total={blueprints.length} bind:limit onchange={() => saveContext()} label="collections" />
|
||
</div>
|
||
<style>
|
||
.page { display: flex; flex-direction: column; gap: 28px; }
|
||
section { display: grid; gap: 16px; }
|
||
h2 { font-size: 18px; margin: 0; } h2 span { opacity: .6; margin-left: 8px; }
|
||
p { margin: 8px 0; }
|
||
.error { color: var(--state-danger); }
|
||
.blueprint p, .blueprint-empty p { color: var(--text-secondary); font-size: 13px; line-height: 1.6; }
|
||
.blueprint-lineup { list-style: none; padding: 0; margin: 8px 0; display: grid; gap: 9px; font-size: 13px; }.blueprint-lineup strong { color: var(--accent-primary-hover); margin-right: 5px; }.blueprint-lineup small { color: var(--text-secondary); margin-left: 6px; }.edit { background: none; border: 0; color: var(--accent-primary-hover); font: inherit; font-size: 12px; cursor: pointer; }.blueprint-footer { flex-wrap: wrap; }
|
||
.eyebrow { display: block; text-transform: uppercase; letter-spacing: .1em; font-size: var(--text-xs); color: var(--text-secondary); margin-bottom: 8px; }
|
||
.blueprints { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(290px, 100%), 1fr)); gap: 16px; }.blueprint { padding: 22px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--surface); display: flex; flex-direction: column; }.blueprint h3 { margin: 8px 0 0; font-size: 19px; overflow-wrap: anywhere; }.blueprint > p { flex: 1; overflow-wrap: anywhere; }.blueprint-top, .blueprint-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; }.blueprint-top .eyebrow { margin: 0; }.file-count { font-size: var(--text-xs); color: var(--text-secondary); white-space: nowrap; }.blueprint-footer { border-top: 1px solid var(--border-subtle); padding-top: 16px; margin-top: 16px; }
|
||
.remove, .blueprint-empty button { background: none; color: var(--accent-primary-hover); border: 0; font: inherit; font-size: 12px; cursor: pointer; }.remove { color: var(--text-secondary); }.blueprint-empty { border: 1px dashed var(--border-strong); border-radius: var(--radius-md); padding: 24px; }.blueprint-empty strong { font-size: 15px; }.blueprint-empty button { padding: 8px 0 0; }
|
||
</style>
|