Initialize calm game project
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
open,
|
||||
title = 'Подтвердите',
|
||||
message = 'Вы уверены?',
|
||||
confirmLabel = 'Да, продолжить',
|
||||
cancelLabel = 'Отмена',
|
||||
onConfirm,
|
||||
onCancel
|
||||
}: Props = $props();
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (!open) return;
|
||||
if (e.key === 'Escape' || e.key === 'Enter') {
|
||||
e.key === 'Enter' ? onConfirm() : onCancel();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeydown} />
|
||||
|
||||
{#if open}
|
||||
<!-- Затемнённый фон -->
|
||||
<div
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-night-950/80 p-4 backdrop-blur-md"
|
||||
transition:fade={{ duration: 200 }}
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-md rounded-3xl border border-white/10 bg-gradient-to-br from-night-700/95 to-night-900/95 p-7 text-center shadow-2xl shadow-night-950/60"
|
||||
transition:scale={{ duration: 220, start: 0.92, opacity: 0.5 }}
|
||||
>
|
||||
<h2 class="text-xl font-bold text-white sm:text-2xl">{title}</h2>
|
||||
<p class="mt-3 text-sm leading-relaxed text-night-50/70">{message}</p>
|
||||
|
||||
<div class="mt-7 flex flex-col gap-3 sm:flex-row sm:justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="rounded-2xl border border-white/15 bg-white/5 px-6 py-3 text-sm font-semibold text-night-50/80 transition-all duration-200 hover:bg-white/10 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-white/20 active:scale-95"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onConfirm}
|
||||
class="rounded-2xl bg-gradient-to-br from-ember-400 to-ember-500 px-6 py-3 text-sm font-bold text-night-900 shadow-lg shadow-ember-500/30 transition-all duration-200 hover:shadow-ember-500/50 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ember-400/50 active:scale-95"
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import type { QuestionRef, QuestionSet } from '$lib/types';
|
||||
import QuestionCard from './QuestionCard.svelte';
|
||||
|
||||
interface Props {
|
||||
set: QuestionSet;
|
||||
/** Множество сыгранных вопросов (`categoryId:questionId`). */
|
||||
played: QuestionRef[];
|
||||
onOpenQuestion: (ref: QuestionRef) => void;
|
||||
}
|
||||
|
||||
let { set, played, onOpenQuestion }: Props = $props();
|
||||
|
||||
const isPlayed = (categoryId: string, questionId: string) =>
|
||||
played.includes(`${categoryId}:${questionId}`);
|
||||
|
||||
// Эмодзи-иконка для строки категории (чисто декоративная, по индексу).
|
||||
const rowIcons = ['🐾', '🌍', '✨'];
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 w-full flex-col gap-2 sm:gap-3">
|
||||
{#each set.categories as category, ci (category.id)}
|
||||
<div class="grid min-h-0 flex-1 gap-2 sm:gap-3" style="grid-template-columns: minmax(7rem, 1fr) repeat(5, minmax(0, 1fr))">
|
||||
<!-- Заголовок категории (левая «колонка» строки) -->
|
||||
<div
|
||||
class="flex min-h-0 items-center gap-2 rounded-2xl border border-plum-400/30 bg-gradient-to-br from-plum-400/15 to-night-700/60 px-3 backdrop-blur-sm sm:px-4"
|
||||
>
|
||||
<span class="text-xl sm:text-2xl">{rowIcons[ci % rowIcons.length]}</span>
|
||||
<h3
|
||||
class="font-bold leading-tight text-plum-300"
|
||||
style="font-size: clamp(0.8rem, 1.6vw, 1.5rem); text-shadow: 0 0 14px rgba(192, 132, 252, 0.3)"
|
||||
>
|
||||
{category.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Карточки очков 100 / 200 / 300 / 500 / 1000 -->
|
||||
{#each category.questions as question (question.id)}
|
||||
{@const ref = `${category.id}:${question.id}`}
|
||||
<div class="min-h-0 animate-pop-in" style="animation-delay: {ci * 50}ms">
|
||||
<QuestionCard
|
||||
points={question.points}
|
||||
played={isPlayed(category.id, question.id)}
|
||||
onSelect={() => onOpenQuestion(ref)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
score: number;
|
||||
playedCount: number;
|
||||
onPlayAgain: () => void;
|
||||
onChangeSet: () => void;
|
||||
}
|
||||
|
||||
let { score, playedCount, onPlayAgain, onChangeSet }: Props = $props();
|
||||
|
||||
// Мягкая финальная фраза в зависимости от результата.
|
||||
const finalPhrase = $derived.by(() => {
|
||||
const per = playedCount > 0 ? score / playedCount : 0;
|
||||
if (per >= 450) return 'Великолепная командная игра — вы блестяще справились!';
|
||||
if (per >= 250) return 'Замечательный вечер! Вы отлично играли вместе.';
|
||||
if (per > 0) return 'Хорошая игра! Главное — вы провели время вместе.';
|
||||
return 'Спасибо за тёплый вечер. Главное — что вы были вместе.';
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="animate-fade-in mx-auto flex min-h-[80vh] w-full max-w-2xl flex-col items-center justify-center px-4 py-10 text-center">
|
||||
<div class="mb-6 animate-pop-in text-6xl" aria-hidden="true">🌟</div>
|
||||
|
||||
<h1
|
||||
class="bg-gradient-to-r from-gold-300 via-ember-300 to-plum-300 bg-clip-text text-3xl font-bold text-transparent sm:text-5xl"
|
||||
>
|
||||
Вечер завершён
|
||||
</h1>
|
||||
|
||||
<!-- Итоговый счёт -->
|
||||
<div
|
||||
class="mt-10 w-full max-w-md rounded-3xl border border-gold-400/30 bg-gradient-to-br from-night-700/80 to-night-800/80 p-8 shadow-2xl shadow-night-950/50 backdrop-blur-sm animate-soft-rise"
|
||||
>
|
||||
<span class="text-xs font-semibold uppercase tracking-widest text-gold-300/80">
|
||||
Общий счёт команды
|
||||
</span>
|
||||
<div
|
||||
class="mt-2 text-6xl font-extrabold tabular-nums text-gold-300 sm:text-7xl animate-score-bump"
|
||||
style="text-shadow: 0 0 30px rgba(251, 191, 36, 0.4)"
|
||||
>
|
||||
{score.toLocaleString('ru-RU')}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-center gap-6 text-sm text-night-50/60">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg">✅</span>
|
||||
<span>{playedCount} вопросов сыграно</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Мягкая финальная фраза -->
|
||||
<p class="mt-8 max-w-md text-lg leading-relaxed text-night-50/80 animate-soft-rise">
|
||||
{finalPhrase}
|
||||
</p>
|
||||
|
||||
<!-- Действия -->
|
||||
<div class="mt-10 flex flex-wrap items-center justify-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onPlayAgain}
|
||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-br from-mint-400 to-mint-300 px-7 py-4 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"
|
||||
>
|
||||
🔄 Сыграть снова
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={onChangeSet}
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/5 px-7 py-4 text-base font-semibold text-night-50/80 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"
|
||||
>
|
||||
🗂️ Выбрать другой набор
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="mt-12 text-sm text-night-50/40">Спокойной ночи и до новых игр 🌙</p>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type { QuestionRef, QuestionSet } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
set: QuestionSet;
|
||||
played: QuestionRef[];
|
||||
onRandom: () => void;
|
||||
onNewGame: () => void;
|
||||
/** Полный сброс к экрану выбора набора. */
|
||||
onChangeSet: () => void;
|
||||
}
|
||||
|
||||
let { set, played, onRandom, onNewGame, onChangeSet }: Props = $props();
|
||||
|
||||
/** Есть ли ещё несыгранные вопросы (для кнопки «Случайный вопрос»). */
|
||||
const hasUnplayed = $derived(
|
||||
set.categories.some((c) => c.questions.some((q) => !played.includes(`${c.id}:${q.id}`)))
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onRandom}
|
||||
disabled={!hasUnplayed}
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-mint-400/40 bg-gradient-to-br from-mint-400/20 to-night-700/60 px-5 py-3 text-sm font-bold text-mint-300 shadow-lg shadow-night-950/30 transition-all duration-200 hover:scale-105 hover:border-mint-400/70 hover:shadow-mint-400/20 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-mint-400/40 active:scale-95 disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:scale-100 sm:text-base"
|
||||
>
|
||||
🎲 <span>Случайный вопрос</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={onNewGame}
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/5 px-5 py-3 text-sm font-semibold text-night-50/80 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 sm:text-base"
|
||||
>
|
||||
🔄 <span>Новая игра</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={onChangeSet}
|
||||
class="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/5 px-5 py-3 text-sm font-semibold text-night-50/80 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 sm:text-base"
|
||||
>
|
||||
🗂️ <span>Сменить набор</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import type { PointValue } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
/** Стоимость вопроса (берётся из данных, НЕ хардкод). */
|
||||
points: PointValue;
|
||||
/** Сыгран ли вопрос (затемняется и неактивен). */
|
||||
played: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
let { points, played, onSelect }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if played}
|
||||
<!-- Сыгранный вопрос: затемнён, неактивен -->
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
aria-disabled="true"
|
||||
aria-label="Вопрос сыгран"
|
||||
class="flex h-full w-full min-h-0 items-center justify-center rounded-2xl border border-white/5 bg-night-900/60 text-night-50/25 transition-colors"
|
||||
>
|
||||
<span class="text-2xl font-bold line-through decoration-night-50/20 lg:text-4xl">✓</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={onSelect}
|
||||
aria-label="Открыть вопрос за {points} очков"
|
||||
class="group relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden rounded-2xl border border-gold-400/30 bg-gradient-to-br from-night-700/90 to-night-800/90 shadow-lg shadow-night-950/40 transition-all duration-300 hover:scale-[1.03] hover:border-gold-400/70 hover:shadow-xl hover:shadow-gold-400/30 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-gold-400/50 active:scale-95"
|
||||
>
|
||||
<!-- свечение при наведении -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-gradient-to-br from-ember-400/0 via-ember-400/0 to-gold-400/0 opacity-0 transition-opacity duration-300 group-hover:from-ember-400/20 group-hover:via-ember-400/5 group-hover:to-gold-400/20 group-hover:opacity-100"
|
||||
></div>
|
||||
<span
|
||||
class="relative font-extrabold tabular-nums text-gold-300 transition-transform duration-300 group-hover:scale-110"
|
||||
style="font-size: clamp(1rem, 3vw, 2.6rem); text-shadow: 0 0 16px rgba(251, 191, 36, 0.35)"
|
||||
>
|
||||
{points.toLocaleString('ru-RU')}
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,164 @@
|
||||
<script lang="ts">
|
||||
import type { Category, Question } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
category: Category;
|
||||
question: Question;
|
||||
onAward: (points: number) => void;
|
||||
onReject: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { category, question, onAward, onReject, onClose }: Props = $props();
|
||||
|
||||
let answerShown = $state(false);
|
||||
let hintShown = $state(false);
|
||||
|
||||
// При смене вопроса — сбрасываем локальное состояние показа.
|
||||
$effect(() => {
|
||||
question.id;
|
||||
answerShown = false;
|
||||
hintShown = false;
|
||||
});
|
||||
|
||||
// Творческие вопросы — мягкие формулировки.
|
||||
const isCreative = $derived(question.type === 'creative');
|
||||
|
||||
// Управление с клавиатуры: Esc — вернуться (если ответ скрыт — закрыть модал,
|
||||
// иначе просто убрать фокус с кнопки). Удобно ведущему.
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && !answerShown) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
</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-label="Вопрос викторины"
|
||||
>
|
||||
<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/20 via-gold-400/20 to-plum-400/20 blur-3xl"
|
||||
></div>
|
||||
|
||||
<div class="relative p-6 sm:p-10">
|
||||
<!-- Категория + стоимость -->
|
||||
<div class="mb-6 flex items-center justify-between gap-4">
|
||||
<span
|
||||
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>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full bg-gold-400/15 px-4 py-1.5 text-lg font-extrabold tabular-nums text-gold-300"
|
||||
style="text-shadow: 0 0 14px rgba(251, 191, 36, 0.4)"
|
||||
>
|
||||
{question.points.toLocaleString('ru-RU')}
|
||||
{#if isCreative}
|
||||
<span class="ml-2 text-xs font-medium text-plum-300">творческий</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Сам вопрос -->
|
||||
<p
|
||||
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 !hintShown}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (hintShown = true)}
|
||||
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}
|
||||
|
||||
<!-- Ответ -->
|
||||
{#if answerShown}
|
||||
<div class="mt-8 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(question.points)}
|
||||
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"
|
||||
>
|
||||
✓ {isCreative ? 'Засчитать творческий ответ' : 'Засчитать'}
|
||||
<span class="tabular-nums">+{question.points.toLocaleString('ru-RU')}</span>
|
||||
</button>
|
||||
|
||||
<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"
|
||||
>
|
||||
✕ Не засчитывать
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onClose}
|
||||
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-8 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={onClose}
|
||||
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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import type { QuestionSet } from '$lib/types';
|
||||
import { getTotalQuestions } from '$lib/question-sets';
|
||||
|
||||
interface Props {
|
||||
sets: QuestionSet[];
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
let { sets, onSelect }: Props = $props();
|
||||
|
||||
let hovered = $state<string | null>(null);
|
||||
</script>
|
||||
|
||||
<div class="animate-fade-in mx-auto w-full max-w-6xl px-4 py-10 sm:py-16">
|
||||
<header class="mb-10 text-center sm:mb-14">
|
||||
<div
|
||||
class="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-3xl bg-gradient-to-br from-gold-400 to-ember-500 text-4xl shadow-lg shadow-ember-500/30 animate-glow-pulse"
|
||||
>
|
||||
🌙
|
||||
</div>
|
||||
<h1
|
||||
class="bg-gradient-to-r from-gold-300 via-ember-300 to-plum-300 bg-clip-text text-3xl font-bold tracking-tight text-transparent sm:text-5xl"
|
||||
>
|
||||
Уютная викторина
|
||||
</h1>
|
||||
<p class="mx-auto mt-4 max-w-xl text-base text-night-50/70 sm:text-lg">
|
||||
Спокойная игра для всей семьи перед сном. Выберите набор вопросов — и устраивайтесь поудобнее на диване.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each sets as set, i (set.id)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onSelect(set.id)}
|
||||
onmouseenter={() => (hovered = set.id)}
|
||||
onmouseleave={() => (hovered = null)}
|
||||
style="--i: {i}"
|
||||
class="group relative flex flex-col overflow-hidden rounded-3xl border border-white/10 bg-white/5 p-6 text-left backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:border-gold-400/50 hover:bg-white/10 hover:shadow-2xl hover:shadow-ember-500/20 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-gold-400/40 animate-soft-rise"
|
||||
>
|
||||
<!-- декоративное свечение -->
|
||||
<div
|
||||
class="pointer-events-none absolute -right-10 -top-10 h-32 w-32 rounded-full bg-gradient-to-br from-ember-400/30 to-plum-400/20 blur-2xl transition-opacity duration-300 group-hover:opacity-100 {hovered ===
|
||||
set.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-50'}"
|
||||
></div>
|
||||
|
||||
<div class="relative mb-4 flex items-center justify-between">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full bg-gold-400/15 px-3 py-1 text-xs font-semibold text-gold-300"
|
||||
>
|
||||
{set.ageRange}
|
||||
</span>
|
||||
<span class="text-2xl opacity-70 transition-transform duration-300 group-hover:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 class="relative text-xl font-bold text-white sm:text-2xl">{set.title}</h2>
|
||||
<p class="relative mt-2 flex-1 text-sm leading-relaxed text-night-50/70">
|
||||
{set.description}
|
||||
</p>
|
||||
|
||||
<div class="relative mt-5 flex items-center gap-4 text-xs text-night-50/50">
|
||||
<span class="inline-flex items-center gap-1">📂 {set.categories.length} категорий</span>
|
||||
<span class="inline-flex items-center gap-1">❓ {getTotalQuestions(set)} вопросов</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<footer class="mt-12 text-center text-xs text-night-50/40">
|
||||
Ведущий управляет игрой · команда отвечает вместе · никто не торопится
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
score: number;
|
||||
/** Триггер для анимации «подпрыгивания» при изменении счёта. */
|
||||
bumpSignal?: number;
|
||||
}
|
||||
|
||||
let { score, bumpSignal = 0 }: Props = $props();
|
||||
|
||||
// Реагируем на каждое изменение сигнала — пересоздаём анимацию.
|
||||
let bumpClass = $state('');
|
||||
$effect(() => {
|
||||
// читаем сигнал
|
||||
bumpSignal;
|
||||
if (score === 0) return;
|
||||
bumpClass = 'animate-score-bump';
|
||||
const t = setTimeout(() => (bumpClass = ''), 500);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-2xl border border-gold-400/30 bg-gradient-to-br from-night-700/80 to-night-800/80 px-5 py-3 shadow-lg shadow-night-950/40 backdrop-blur-sm"
|
||||
>
|
||||
<span class="text-2xl" aria-hidden="true">⭐</span>
|
||||
<div class="flex flex-col leading-none">
|
||||
<span class="text-[10px] font-semibold uppercase tracking-widest text-gold-300/80">
|
||||
Счёт команды
|
||||
</span>
|
||||
<span
|
||||
class="text-3xl font-extrabold tabular-nums text-gold-300 sm:text-4xl {bumpClass}"
|
||||
style="text-shadow: 0 0 18px rgba(251, 191, 36, 0.4)"
|
||||
>
|
||||
{score.toLocaleString('ru-RU')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user