Files
my-calm-game/src/lib/components/AuctionModal.svelte
T

302 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 { untrack } from 'svelte';
import type { Category, Question } from '$lib/types';
interface Props {
/** Номинал оригинальной карточки табло, по которой запустили аукцион. */
originalNominal: number;
/** Название набора, откуда пришёл случайный вопрос аукциона. */
sourceSetTitle: string;
/** Категория случайного вопроса аукциона. */
category: Category;
/** Случайный вопрос аукциона (не с табло). */
question: Question;
/** Текущий счёт команды. */
score: number;
/** Текущая ставка — эффективная стоимость аукциона. */
wager: number;
/** Показывал ли ведущий подсказку аукциона. */
hintUsed: boolean;
onSetWager: (wager: number) => void;
onMarkHintUsed: () => void;
onAward: (points: number) => void;
onReject: () => void;
onCancel: () => void;
}
let {
originalNominal,
sourceSetTitle,
category,
question,
score,
wager,
hintUsed,
onSetWager,
onMarkHintUsed,
onAward,
onReject,
onCancel
}: Props = $props();
let answerShown = $state(false);
// При смене вопроса аукциона — сбрасываем локальное состояние показа ответа.
$effect(() => {
question.id;
answerShown = false;
});
// Ставку можно редактировать только при ненулевом счёте.
const wagerEditable = $derived(score > 0);
// Локальное зеркало поля ставки: даём свободно печатать, не сражаясь с
// пользователем, и синхронизируемся со значением из состояния, когда оно
// поменялось извне (кнопка «Ва-банк» или восстановление после перезагрузки).
let wagerInput = $state(untrack(() => String(wager)));
let lastWager = untrack(() => wager);
$effect(() => {
if (wager !== lastWager) {
lastWager = wager;
wagerInput = String(wager);
}
});
function onWagerInput(e: Event) {
const target = e.currentTarget as HTMLInputElement;
wagerInput = target.value;
const n = parseInt(target.value, 10);
// Пустое/нечисловое поле коммитим в состояние только когда оно валидно —
// даём ведущему спокойно стереть и перепечатать ставку.
if (Number.isFinite(n)) {
onSetWager(n);
}
}
function vaBank() {
onSetWager(score);
}
// Половина ставки — для ответа по подсказке или частично правильного ответа.
const halfValue = $derived(Math.round(wager / 2));
// Сколько очков даёт основное «Засчитать»: с подсказкой — половину, иначе ставку.
const awardPoints = $derived(hintUsed ? halfValue : wager);
// Управление с клавиатуры: Esc — отменить аукцион (если ответ скрыт).
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && !answerShown) {
onCancel();
}
}
</script>
<svelte:window onkeydown={onKeydown} />
<!-- Затемнённый фон -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-night-950/80 p-4 backdrop-blur-md animate-fade-in"
role="dialog"
aria-modal="true"
aria-labelledby="auction-modal-title"
aria-describedby="auction-modal-value"
>
<div
class="relative w-full max-w-3xl overflow-hidden rounded-3xl border border-white/10 bg-gradient-to-br from-night-700/95 to-night-900/95 shadow-2xl shadow-night-950/60 animate-pop-in"
>
<!-- верхняя подсветка — тёплая, «аукционная» -->
<div
class="pointer-events-none absolute -top-24 left-1/2 h-48 w-[120%] -translate-x-1/2 rounded-full bg-gradient-to-r from-ember-400/25 via-gold-400/25 to-ember-400/25 blur-3xl"
></div>
<div class="relative p-6 sm:p-10">
<!-- Аукцион + источник + категория -->
<div class="mb-5 flex flex-wrap items-center justify-between gap-3">
<div class="flex min-w-0 items-center gap-2">
<span
class="inline-flex items-center rounded-full bg-ember-400/15 px-4 py-1.5 text-sm font-semibold text-ember-300"
>
🔨 Аукцион
</span>
<span class="truncate text-xs text-night-50/50">из «{sourceSetTitle}»</span>
</div>
<span
id="auction-modal-value"
class="inline-flex items-center rounded-full bg-plum-400/15 px-4 py-1.5 text-sm font-semibold text-plum-300"
>
{category.title}
</span>
</div>
<!-- Сам вопрос -->
<p
id="auction-modal-title"
class="mb-2 text-center text-2xl font-bold leading-snug text-white sm:text-4xl"
style="text-shadow: 0 0 20px rgba(255,255,255,0.08)"
>
{question.question}
</p>
<!-- Подсказка (необязательная) -->
{#if question.optionalHint}
<div class="mt-4 flex justify-center">
{#if !hintUsed}
<button
type="button"
onclick={onMarkHintUsed}
class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm text-night-50/70 transition-colors hover:bg-white/10"
>
💡 Показать подсказку
</button>
{:else}
<p class="animate-fade-in max-w-lg rounded-2xl bg-white/5 px-4 py-3 text-center text-sm italic text-night-50/70">
💡 {question.optionalHint}
</p>
{/if}
</div>
{/if}
<!-- Управление ставкой -->
<div class="mt-6 rounded-2xl border border-white/10 bg-white/5 p-4">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="text-sm text-night-50/70">
<div class="text-[10px] uppercase tracking-widest text-night-50/50">
Карточка табло
</div>
<div class="font-bold tabular-nums text-gold-300">
{originalNominal.toLocaleString('ru-RU')} очков
</div>
</div>
<div class="flex items-center gap-3">
{#if wagerEditable}
<label for="auction-wager" class="text-[10px] uppercase tracking-widest text-night-50/50">
Ставка
</label>
<input
id="auction-wager"
type="number"
inputmode="numeric"
min="1"
max={score}
value={wagerInput}
oninput={onWagerInput}
aria-label="Ставка аукциона"
class="w-28 rounded-xl border border-white/15 bg-night-900/60 px-3 py-2 text-center text-lg font-bold tabular-nums text-white focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-gold-400/40"
/>
<button
type="button"
onclick={vaBank}
class="inline-flex items-center gap-1 rounded-xl border border-ember-400/50 bg-ember-400/15 px-4 py-2 text-sm font-bold text-ember-300 transition-all duration-200 hover:scale-105 hover:bg-ember-400/25 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ember-400/40 active:scale-95"
>
🔥 Ва-банк<span class="tabular-nums"> {score.toLocaleString('ru-RU')}</span>
</button>
{:else}
<div class="text-sm text-night-50/70">
<div class="text-[10px] uppercase tracking-widest text-night-50/50">
Ставка
</div>
<div class="font-bold tabular-nums text-gold-300">
{wager.toLocaleString('ru-RU')} очков
</div>
</div>
{/if}
</div>
</div>
{#if !wagerEditable}
<p class="mt-2 text-center text-xs text-night-50/50">
Счёт 0 — ставка равна номиналу карточки и не редактируется.
</p>
{/if}
</div>
<!-- Ответ -->
{#if answerShown}
<div class="mt-6 animate-reveal-answer">
<div class="rounded-2xl border border-mint-400/30 bg-mint-400/10 p-5 text-center">
<span class="mb-1 block text-xs font-semibold uppercase tracking-widest text-mint-300/80">
Ответ
</span>
<p class="text-xl font-bold text-mint-300 sm:text-2xl">{question.answer}</p>
</div>
<!-- Действия ведущего -->
<div class="mt-6 flex flex-wrap items-center justify-center gap-3">
<button
type="button"
onclick={() => onAward(awardPoints)}
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-br from-mint-400 to-mint-300 px-6 py-3.5 text-base font-bold text-night-900 shadow-lg shadow-mint-400/30 transition-all duration-200 hover:scale-105 hover:shadow-mint-400/50 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-mint-400/50 active:scale-95"
>
✓ Засчитать
<span class="tabular-nums">+{awardPoints.toLocaleString('ru-RU')}</span>
</button>
<!-- Частично правильный ответ: половина ставки. Доступно только если
подсказка НЕ показывалась (иначе основная кнопка уже даёт половину). -->
{#if !hintUsed}
<button
type="button"
onclick={() => onAward(halfValue)}
class="inline-flex items-center gap-2 rounded-2xl border border-mint-400/40 bg-mint-400/10 px-5 py-3.5 text-base font-semibold text-mint-300 transition-all duration-200 hover:scale-105 hover:border-mint-400/60 hover:bg-mint-400/20 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-mint-400/30 active:scale-95"
>
≈ Засчитать частично
<span class="tabular-nums">+{halfValue.toLocaleString('ru-RU')}</span>
</button>
{/if}
<button
type="button"
onclick={onReject}
class="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/5 px-6 py-3.5 text-base font-semibold text-night-50/70 transition-all duration-200 hover:scale-105 hover:border-white/30 hover:bg-white/10 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-white/20 active:scale-95"
>
✕ Не засчитывать
{#if wagerEditable}
<span class="tabular-nums text-night-50/50">{wager.toLocaleString('ru-RU')}</span>
{/if}
</button>
</div>
{#if hintUsed}
<p class="mt-3 text-center text-xs text-night-50/50">
💡 Подсказка показана — ответ засчитывается за половину ставки.
</p>
{:else if !wagerEditable}
<p class="mt-3 text-center text-xs text-night-50/50">
Счёт 0 — неправильный ответ не меняет очки.
</p>
{/if}
<div class="mt-4 text-center">
<button
type="button"
onclick={onCancel}
class="text-sm text-night-50/50 underline-offset-4 transition-colors hover:text-night-50/80 hover:underline"
>
← Вернуться к табло (отменить аукцион)
</button>
</div>
</div>
{:else}
<div class="mt-6 flex justify-center">
<button
type="button"
onclick={() => (answerShown = true)}
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-br from-gold-400 to-ember-500 px-8 py-4 text-lg font-bold text-night-900 shadow-lg shadow-gold-400/30 transition-all duration-200 hover:scale-105 hover:shadow-gold-400/50 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-gold-400/50 active:scale-95"
>
👁️ Показать ответ
</button>
</div>
<div class="mt-4 text-center">
<button
type="button"
onclick={onCancel}
class="text-sm text-night-50/50 underline-offset-4 transition-colors hover:text-night-50/80 hover:underline"
>
← Вернуться к табло (отменить аукцион)
</button>
</div>
{/if}
</div>
</div>
</div>