Files
website/src/lib/pc/EditDialogs.svelte
Stan Runge eff5a24985
All checks were successful
Deploy production / deploy (push) Successful in 1m7s
pc setup v2: one-screen layout with a time slider, photo grid and lightbox
- hardware: slots, sections, usage, install/removal dates, images and a hidden flag on
  components and devices; setup_photos table; setup_overview view gains the new columns
- data: hide the 2021 friends-PC build and the wrong 140mm / unknown fans (kept, not deleted);
  7x SL-INF + 3 Kraken stock fans; mice, audio, desk extras; MacBook Pro 16 M2 Pro 1 TB;
  dates for every swap so the slider can rebuild the setup on any day
- self-hosted product images in static/hardware (webp + thumbs)
- prices and order details are owner-only (in a popover)
2026-10-25 16:16:38 +00:00

436 lines
12 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { enhance } from '$app/forms';
import type { SubmitFunction } from '@sveltejs/kit';
import Modal from './Modal.svelte';
import {
CATEGORIES,
DEVICE_KINDS,
DEVICE_KIND_META,
DEVICE_STATUSES,
PART_STATUSES,
SECTIONS,
SLOT_META,
USAGES,
categoryLabel,
specsToText,
statusLabel,
type Device,
type Game,
type Part
} from './shared';
// Owner-only edit dialogs for parts, devices and games (the page sets one of them to open it).
let {
editPart = $bindable(null),
editDevice = $bindable(null),
editGame = $bindable(null),
devices,
mice
}: {
editPart?: Partial<Part> | null;
editDevice?: Partial<Device> | null;
editGame?: Partial<Game> | null;
devices: Device[];
mice: Part[];
} = $props();
let problem = $state('');
const submit =
(close: () => void): SubmitFunction =>
() => {
problem = '';
return async ({ result, update }) => {
if (result.type === 'failure')
problem = String((result.data as { message?: string })?.message ?? 'Could not save');
else if (result.type === 'success') {
close();
await update({ reset: false });
} else await update();
};
};
const confirmDelete = (what: string) => (event: SubmitEvent) => {
if (!confirm(`Delete ${what}?`)) event.preventDefault();
};
const euros = (cents: number | null | undefined) =>
cents === null || cents === undefined ? '' : (cents / 100).toFixed(2);
</script>
{#if editPart}
{@const p = editPart}
<Modal
title={p.id ? `Edit ${p.model}` : 'Add a part'}
onclose={() => {
editPart = null;
problem = '';
}}
>
{#snippet children(close)}
<form method="POST" action="?/savePart" use:enhance={submit(close)} class="form">
<input type="hidden" name="id" value={p.id ?? ''} />
<label
>device<select name="deviceId" value={p.deviceId}>
{#each devices as d (d.id)}<option value={d.id}>{d.label ?? d.name}</option>{/each}
</select></label
>
<label
>category<select name="category" value={p.category}>
{#each CATEGORIES as c (c)}<option value={c}>{categoryLabel(c)}</option>{/each}
</select></label
>
<label>brand<input name="brand" value={p.brand ?? ''} maxlength="120" /></label>
<label class="wide"
>model<input name="model" value={p.model ?? ''} required maxlength="300" /></label
>
<label
>quantity<input name="quantity" type="number" min="1" value={p.quantity ?? 1} /></label
>
<label
>price per unit (€)<input
name="unitPrice"
inputmode="decimal"
value={euros(p.unitPriceCents)}
/></label
>
<label>shop<input name="shop" value={p.shop ?? ''} /></label>
<label>order date<input name="orderDate" type="date" value={p.orderDate ?? ''} /></label>
<label
>warranty until<input
name="warrantyUntil"
type="date"
value={p.warrantyUntil ?? ''}
/></label
>
<label
>status<select name="status" value={p.status}>
{#each PART_STATUSES as s (s)}<option value={s}>{statusLabel(s)}</option>{/each}
</select></label
>
<label>order number<input name="orderNumber" value={p.orderNumber ?? ''} /></label>
<label>invoice number<input name="invoiceNumber" value={p.invoiceNumber ?? ''} /></label>
<label>DPI (set)<input name="dpi" type="number" min="1" value={p.dpi ?? ''} /></label>
<label
>DPI (measured)<input
name="measuredDpi"
type="number"
min="1"
value={p.measuredDpi ?? ''}
/></label
>
<label class="wide">link<input name="url" type="url" value={p.url ?? ''} /></label>
<input type="hidden" name="imageUrl" value={p.imageUrl ?? ''} />
<label
>section<select name="section" value={p.section ?? ''}>
<option value="">–</option>
{#each SECTIONS as s (s)}<option value={s}>{s}</option>{/each}
</select></label
>
<label
>slot<input
name="slot"
value={p.slot ?? ''}
list="pc-slots"
placeholder="cpu, fans-case, mouse-gaming…"
/></label
>
<datalist id="pc-slots">
{#each Object.keys(SLOT_META) as s (s)}<option value={s}></option>{/each}
</datalist>
<label
>usage<select name="usage" value={p.usage ?? ''}>
<option value="">–</option>
{#each USAGES as u (u)}<option value={u}>{u}</option>{/each}
</select></label
>
<label
>in setup since<input name="installedAt" type="date" value={p.installedAt ?? ''} /></label
>
<label>removed on<input name="removedAt" type="date" value={p.removedAt ?? ''} /></label>
<label class="wide"
>images (one per line, /hardware/… or https://)<textarea name="imageUrls" rows="3"
>{(p.imageUrls ?? []).join('\n')}</textarea
></label
>
<label class="wide"
>specs (one "key: value" per line)<textarea name="specs" rows="4"
>{specsToText(p.specs ?? {})}</textarea
></label
>
<label class="wide"
>notes (only you)<textarea name="notes" rows="2">{p.notes ?? ''}</textarea></label
>
<label class="check"
><input type="checkbox" name="verified" checked={p.verified ?? true} /> confirmed still in this
setup</label
>
<label class="check"
><input type="checkbox" name="hidden" checked={p.hidden ?? false} /> hidden (kept for history,
not shown)</label
>
{#if problem}<p class="wide text-sm text-red-500" role="alert">{problem}</p>{/if}
<div class="buttons wide">
<button type="button" class="action" onclick={close}>Cancel</button>
<button type="submit" class="action primary">Save</button>
</div>
</form>
{#if p.id}
<form
method="POST"
action="?/deletePart"
use:enhance={submit(close)}
onsubmit={confirmDelete(p.model ?? 'this part')}
class="mt-2"
>
<input type="hidden" name="id" value={p.id} />
<button type="submit" class="text-sm text-red-500 hover:underline">Delete part</button>
</form>
{/if}
{/snippet}
</Modal>
{/if}
{#if editDevice}
{@const d = editDevice}
<Modal
title={d.id ? `Edit ${d.name}` : 'Add a device'}
onclose={() => {
editDevice = null;
problem = '';
}}
>
{#snippet children(close)}
<form method="POST" action="?/saveDevice" use:enhance={submit(close)} class="form">
<input type="hidden" name="id" value={d.id ?? ''} />
<label
>name (hostname)<input
name="name"
value={d.name ?? ''}
required
pattern="[a-z0-9][a-z0-9\-]*"
/></label
>
<label>label<input name="label" value={d.label ?? ''} /></label>
<label
>kind<select name="kind" value={d.kind}>
{#each DEVICE_KINDS as k (k)}<option value={k}>{DEVICE_KIND_META[k].label}</option
>{/each}
</select></label
>
<label
>status<select name="status" value={d.status}>
{#each DEVICE_STATUSES as s (s)}<option value={s}>{s}</option>{/each}
</select></label
>
<label>brand<input name="brand" value={d.brand ?? ''} /></label>
<label>model<input name="model" value={d.model ?? ''} /></label>
<label class="wide"
>other hostnames (comma separated)<input
name="hostnames"
value={(d.hostnames ?? []).join(', ')}
/></label
>
<label
>price (€)<input name="price" inputmode="decimal" value={euros(d.priceCents)} /></label
>
<label
>per month (€)<input
name="monthly"
inputmode="decimal"
value={euros(d.monthlyCents)}
/></label
>
<label
>budget (€)<input name="budget" inputmode="decimal" value={euros(d.budgetCents)} /></label
>
<label>shop<input name="shop" value={d.shop ?? ''} /></label>
<label>since<input name="purchaseDate" type="date" value={d.purchaseDate ?? ''} /></label>
<label
>sort order<input
name="sortOrder"
type="number"
min="0"
value={d.sortOrder ?? 100}
/></label
>
<label class="wide">link<input name="url" type="url" value={d.url ?? ''} /></label>
<label class="wide"
>specs (one "key: value" per line)<textarea name="specs" rows="4"
>{specsToText(d.specs ?? {})}</textarea
></label
>
<label
>in setup since<input name="installedAt" type="date" value={d.installedAt ?? ''} /></label
>
<label>removed on<input name="removedAt" type="date" value={d.removedAt ?? ''} /></label>
<label class="wide"
>images (one per line)<textarea name="imageUrls" rows="2"
>{(d.imageUrls ?? []).join('\n')}</textarea
></label
>
<label class="wide"
>notes (public)<textarea name="notes" rows="3">{d.notes ?? ''}</textarea></label
>
<label class="check"
><input type="checkbox" name="hidden" checked={d.hidden ?? false} /> hidden (kept for history,
not shown)</label
>
{#if problem}<p class="wide text-sm text-red-500" role="alert">{problem}</p>{/if}
<div class="buttons wide">
<button type="button" class="action" onclick={close}>Cancel</button>
<button type="submit" class="action primary">Save</button>
</div>
</form>
{#if d.id}
<form
method="POST"
action="?/deleteDevice"
use:enhance={submit(close)}
onsubmit={confirmDelete(`${d.name} and all its parts`)}
class="mt-2"
>
<input type="hidden" name="id" value={d.id} />
<button type="submit" class="text-sm text-red-500 hover:underline"
>Delete device and its parts</button
>
</form>
{/if}
{/snippet}
</Modal>
{/if}
{#if editGame}
{@const g = editGame}
<Modal
title={g.id ? `Edit ${g.game}` : 'Add a game'}
onclose={() => {
editGame = null;
problem = '';
}}
>
{#snippet children(close)}
<form method="POST" action="?/saveGame" use:enhance={submit(close)} class="form">
<input type="hidden" name="id" value={g.id ?? ''} />
<label class="wide">game<input name="game" value={g.game ?? ''} required /></label>
<label>cm/360<input name="cmPer360" inputmode="decimal" value={g.cmPer360 ?? ''} /></label>
<label
>yaw (°/count at sens 1)<input
name="yaw"
inputmode="decimal"
value={g.yaw ?? ''}
/></label
>
<label
>in-game sens now<input
name="inGameSens"
inputmode="decimal"
value={g.inGameSens ?? ''}
/></label
>
<label
>set with mouse<select name="mouseId" value={g.mouseId ?? ''}>
<option value="">–</option>
{#each mice as m (m.id)}<option value={m.id}>{m.brand} {m.model}</option>{/each}
</select></label
>
<label>FOV<input name="fov" value={g.fov ?? ''} /></label>
<label
>sort order<input
name="sortOrder"
type="number"
min="0"
value={g.sortOrder ?? 100}
/></label
>
<label class="wide"
>notes (only you)<textarea name="notes" rows="2">{g.notes ?? ''}</textarea></label
>
{#if problem}<p class="wide text-sm text-red-500" role="alert">{problem}</p>{/if}
<div class="buttons wide">
<button type="button" class="action" onclick={close}>Cancel</button>
<button type="submit" class="action primary">Save</button>
</div>
</form>
{#if g.id}
<form
method="POST"
action="?/deleteGame"
use:enhance={submit(close)}
onsubmit={confirmDelete(g.game ?? 'this game')}
class="mt-2"
>
<input type="hidden" name="id" value={g.id} />
<button type="submit" class="text-sm text-red-500 hover:underline">Delete game</button>
</form>
{/if}
{/snippet}
</Modal>
{/if}
<style>
.form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.7rem;
}
.form label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.78rem;
color: var(--site-muted);
}
.form .wide,
.form .check,
.form .buttons {
grid-column: 1 / -1;
}
.form .check {
flex-direction: row;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--site-text);
}
.form input:not([type='checkbox']),
.form select,
.form textarea {
border: 1px solid var(--site-border);
border-radius: 0.5rem;
background: var(--site-bg);
color: var(--site-text);
padding: 0.4rem 0.6rem;
font-size: 0.9rem;
}
.form textarea {
font-family: ui-monospace, monospace;
font-size: 0.8rem;
}
.form .buttons {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.action {
display: inline-flex;
align-items: center;
gap: 0.3rem;
border: 1px solid var(--site-border);
border-radius: 0.6rem;
padding: 0.35rem 0.7rem;
font-size: 0.85rem;
transition:
background 0.15s,
border-color 0.15s;
}
.action:hover {
background: var(--site-hover);
}
.action.primary {
background: var(--site-text);
color: var(--site-bg);
border-color: var(--site-text);
}
.action.primary:hover {
opacity: 0.85;
}
</style>