Expand website and game trackers
Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s

This commit is contained in:
2026-09-25 22:43:04 +00:00
parent f6b34150d4
commit c2ba8febb4
4076 changed files with 97237 additions and 1453 deletions

View File

@@ -0,0 +1,183 @@
<script lang="ts">
import { pokemonImage } from '$lib/game-tracker/game-art';
import { enhance } from '$app/forms';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Textarea } from '$lib/components/ui/textarea/index.js';
import { NativeSelect } from '$lib/components/ui/native-select/index.js';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { stages, obtainedMethods, type Stage } from './journal';
import type { PokemonCatch } from '$lib/server/db/schema/tracker-journal';
let {
open = $bindable(false),
species,
gameId,
record = null,
stage = 'story',
location = '',
signedIn,
ondelete,
ondetails
}: {
open?: boolean;
species: { id: number; number: number; name: string };
gameId: number;
record?: PokemonCatch | null;
stage?: Stage;
location?: string;
signedIn: boolean;
ondelete?: () => void;
ondetails?: () => void;
} = $props();
let message = $state('');
let saving = $state(false);
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-h-[90dvh] overflow-y-auto sm:max-w-2xl">
<Dialog.Header
><Dialog.Title>{record ? 'Meet' : 'Register'} {record?.nickname || species.name}</Dialog.Title
><Dialog.Description
>#{species.number} · {species.name}. Keep this Pokémon’s story, moves and party position
together.</Dialog.Description
></Dialog.Header
>
{#if ondetails}<Button variant="link" class="justify-self-start px-0" onclick={ondetails}
>All catches, moves and species research →</Button
>{/if}
{#if message}<p role="alert" class="text-destructive">{message}</p>{/if}
<form
method="POST"
action="?/saveCatch"
class="space-y-5"
use:enhance={() => {
saving = true;
message = '';
return async ({ result, update }) => {
await update({ reset: false });
saving = false;
if (result.type === 'success') open = false;
else
message =
result.type === 'failure'
? String(result.data?.message ?? 'Could not save.')
: 'Could not save. Try again.';
};
}}
>
<input type="hidden" name="gameId" value={gameId} /><input
type="hidden"
name="pokemonId"
value={species.id}
/>{#if record}<input type="hidden" name="id" value={record.id} />{/if}
<div class="flex items-center gap-4 rounded-xl bg-muted/40 p-3">
<img
src={pokemonImage(species.number, record?.shiny ? 'shiny' : 'sprite')}
alt={species.name}
width="80"
height="80"
/>
<div class="grid flex-1 grid-cols-[1fr_5rem] gap-3">
<label
>Nickname<Input
name="nickname"
value={record?.nickname ?? ''}
maxlength={50}
placeholder={species.name}
/></label
><label
>Level<Input
type="number"
name="level"
min={1}
max={100}
required
value={record?.level ?? 5}
/></label
>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<label
>Party position<NativeSelect
name="partySlot"
value={record?.partySlot ?? ''}
class="mt-1 w-full"
><option value="">In the PC</option>{#each [1, 2, 3, 4, 5, 6] as slot (slot)}<option
value={slot}>Team slot {slot}</option
>{/each}</NativeSelect
><span class="text-xs text-muted-foreground"
>An occupied slot moves its current Pokémon to the PC.</span
></label
><label
>Obtained by<NativeSelect
name="obtained"
value={record?.obtained ?? 'caught'}
class="mt-1 w-full"
>{#each obtainedMethods as method (method)}<option value={method}>{method}</option
>{/each}</NativeSelect
></label
>
</div>
<fieldset>
<legend class="mb-2 font-medium">Current moves</legend>
<div class="grid grid-cols-2 gap-3">
{#each [0, 1, 2, 3] as slot (slot)}<Input
name="moves"
aria-label={`Move ${slot + 1}`}
placeholder={`Move ${slot + 1}`}
value={record?.moves[slot] ?? ''}
maxlength={60}
/>{/each}
</div>
</fieldset>
<div class="grid gap-4 sm:grid-cols-2">
<label>Nature<Input name="nature" value={record?.nature ?? ''} maxlength={150} /></label
><label>Ability<Input name="ability" value={record?.ability ?? ''} maxlength={150} /></label
><label>Poké Ball<Input name="ball" value={record?.ball ?? ''} maxlength={150} /></label
><label
>Caught on<Input
type="date"
name="caughtOn"
value={record?.caughtOn ?? new Date().toISOString().slice(0, 10)}
/></label
><label
>Location<Input
name="location"
value={record?.location ?? location}
maxlength={150}
/></label
><label
>Collection stage<NativeSelect name="stage" value={record?.stage ?? stage} class="w-full"
>{#each stages as s (s.value)}<option value={s.value}>{s.label}</option
>{/each}</NativeSelect
></label
>
</div>
<label class="flex items-center gap-2"
><Checkbox name="shiny" value="on" checked={record?.shiny ?? false} />Shiny Pokémon</label
>
<label class="block"
>Notes<Textarea
name="notes"
value={record?.notes ?? ''}
maxlength={2000}
placeholder="A lucky encounter, a trade partner, a plan for evolution…"
/></label
>
<Dialog.Footer
><Button variant="outline" onclick={() => (open = false)}>Cancel</Button><Button
type="submit"
disabled={!signedIn || saving}
>{saving ? 'Saving…' : record ? 'Save Pokémon' : 'Add to my collection'}</Button
></Dialog.Footer
>
</form>
{#if record && ondelete}<Button
variant="ghost"
class="justify-self-start text-destructive"
onclick={ondelete}>Remove this catch record</Button
>{/if}
</Dialog.Content>
</Dialog.Root>