Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
170 lines
4.2 KiB
Svelte
170 lines
4.2 KiB
Svelte
<script lang="ts">
|
|
import type { Duel } from '$lib/game/duel.svelte';
|
|
import { api } from '$lib/game/client';
|
|
|
|
let { duel, open = $bindable(false) }: { duel: Duel; open?: boolean } = $props();
|
|
|
|
let happened = $state('');
|
|
let expected = $state('');
|
|
let image = $state<File | null>(null);
|
|
let preview = $state<string | null>(null);
|
|
let busy = $state(false);
|
|
let sent = $state(false);
|
|
let error = $state('');
|
|
|
|
function pickImage(e: Event) {
|
|
const file = (e.currentTarget as HTMLInputElement).files?.[0] ?? null;
|
|
if (preview) URL.revokeObjectURL(preview);
|
|
if (file && file.size > 2_500_000) {
|
|
error = 'That picture is over 2.5 MB; a smaller one, please.';
|
|
image = null;
|
|
preview = null;
|
|
return;
|
|
}
|
|
error = '';
|
|
image = file;
|
|
preview = file ? URL.createObjectURL(file) : null;
|
|
}
|
|
|
|
function close() {
|
|
open = false;
|
|
if (preview) URL.revokeObjectURL(preview);
|
|
happened = '';
|
|
expected = '';
|
|
image = null;
|
|
preview = null;
|
|
sent = false;
|
|
error = '';
|
|
}
|
|
|
|
async function send(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
const r = duel.remote;
|
|
if (!r || !happened.trim() || busy) return;
|
|
busy = true;
|
|
error = '';
|
|
try {
|
|
const { id } = await api.report(r.roomId, r.token, happened.trim(), expected.trim());
|
|
if (image) await api.reportImage(id, image).catch(() => (error = 'The report went; the picture did not.'));
|
|
sent = true;
|
|
setTimeout(close, 2200);
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : 'The report did not reach the keeper.';
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if open && duel.remote}
|
|
<div class="scrim" role="presentation" onclick={(ev) => ev.target === ev.currentTarget && close()}>
|
|
<form class="slip" aria-labelledby="report-heading" onsubmit={send}>
|
|
<h2 id="report-heading">Something amiss?</h2>
|
|
{#if sent}
|
|
<p class="thanks">Recorded beside the room's ledger, with the turn. Thank you. The keeper will look into it.{#if error} {error}{/if}</p>
|
|
{:else}
|
|
<label class="field">
|
|
<span>What happened?</span>
|
|
<textarea bind:value={happened} rows="3" maxlength="2000" placeholder="e.g. my fireball landed but Wynne took no damage"></textarea>
|
|
</label>
|
|
<label class="field">
|
|
<span>What did you expect?</span>
|
|
<textarea bind:value={expected} rows="2" maxlength="2000" placeholder="e.g. five points of damage"></textarea>
|
|
</label>
|
|
<label class="field picture">
|
|
<span>A screenshot, if you have one</span>
|
|
<input type="file" accept="image/png,image/jpeg,image/webp" onchange={pickImage} />
|
|
{#if preview}<img class="preview" src={preview} alt="The screenshot you chose" />{/if}
|
|
</label>
|
|
<p class="muted small">The room code and the turn are sent with it, so the keeper can replay the duel to this moment.</p>
|
|
{#if error}<p class="warning">{error}</p>{/if}
|
|
<div class="actions">
|
|
<button type="submit" class="reveal small" disabled={!happened.trim() || busy}>{busy ? 'Sending' : 'Send it'}</button>
|
|
<button type="button" class="link" onclick={close}>never mind</button>
|
|
</div>
|
|
{/if}
|
|
</form>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.slip {
|
|
width: min(100%, 30rem);
|
|
max-height: 100%;
|
|
overflow-y: auto;
|
|
background: var(--slate);
|
|
border: 1px solid var(--rule-strong);
|
|
border-radius: 6px;
|
|
padding: 1rem 1.2rem 1.2rem;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1.3rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
margin-top: 0.7rem;
|
|
}
|
|
|
|
textarea {
|
|
font: inherit;
|
|
color: var(--bone);
|
|
background: var(--slate-deep);
|
|
border: 1px solid var(--rule-strong);
|
|
border-radius: 4px;
|
|
padding: 0.4rem 0.6rem;
|
|
resize: vertical;
|
|
}
|
|
|
|
textarea:focus-visible {
|
|
outline: 2px solid var(--frost);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.picture input {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.preview {
|
|
max-height: 8rem;
|
|
max-width: 100%;
|
|
border: 1px solid var(--rule);
|
|
border-radius: 3px;
|
|
margin-top: 0.3rem;
|
|
}
|
|
|
|
.small {
|
|
font-size: 0.85rem;
|
|
margin-top: 0.6rem;
|
|
}
|
|
|
|
.warning {
|
|
color: var(--blood);
|
|
margin-top: 0.4rem;
|
|
}
|
|
|
|
.thanks {
|
|
color: var(--bone);
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-top: 0.9rem;
|
|
}
|
|
|
|
.link {
|
|
background: none;
|
|
border: 0;
|
|
color: var(--bone-dim);
|
|
text-decoration: underline dotted;
|
|
padding: 0;
|
|
font: inherit;
|
|
}
|
|
</style>
|