- Updated API types to replace `GameStub` with `GameInfo` and added `SessionSpinResult` and `SpinOutcome` types. - Modified API requests in `api.ts` to use the new game types. - Implemented the `ForestFortuneGame` component with game logic, including spinning mechanics and win animations. - Created supporting components: `CoinFlight`, `ReelColumn`, `WinCounter`, and `WinLinesOverlay` for game UI. - Added CSS styles for the Forest Fortune game layout and animations. - Updated `GamePage` and `HomePage` to integrate the new game and reflect its status correctly.
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { Link, Navigate } from 'react-router-dom'
|
||
import { api } from '../api'
|
||
import { useAuth } from '../auth'
|
||
import type { GameInfo } from '../types'
|
||
import { GATE_FROM_KEY } from '../vfx/gateCaps'
|
||
|
||
const DEPOSIT_AMOUNTS = [100, 500, 1000]
|
||
|
||
export function HomePage() {
|
||
const { user, loading, setUser } = useAuth()
|
||
const [games, setGames] = useState<GameInfo[]>([])
|
||
const [depositOpen, setDepositOpen] = useState(false)
|
||
const [busy, setBusy] = useState(false)
|
||
const [message, setMessage] = useState<string | null>(null)
|
||
const [fromGate, setFromGate] = useState(false)
|
||
|
||
useEffect(() => {
|
||
void api.listGames().then(setGames).catch(() => setGames([]))
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
try {
|
||
if (sessionStorage.getItem(GATE_FROM_KEY) === '1') {
|
||
sessionStorage.removeItem(GATE_FROM_KEY)
|
||
setFromGate(true)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}, [])
|
||
|
||
if (!loading && user && !user.agreement_accepted) {
|
||
return <Navigate to="/" replace />
|
||
}
|
||
|
||
if (!loading && !user) {
|
||
return <Navigate to="/" replace />
|
||
}
|
||
|
||
async function deposit(amount: number) {
|
||
setBusy(true)
|
||
setMessage(null)
|
||
try {
|
||
const wallet = await api.deposit(amount)
|
||
if (user) {
|
||
setUser({ ...user, balance: wallet.balance })
|
||
}
|
||
setMessage(`Зачислено ${amount}`)
|
||
setDepositOpen(false)
|
||
} catch (err) {
|
||
setMessage(err instanceof Error ? err.message : 'Ошибка пополнения')
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<main className={fromGate ? 'home home--from-gate' : 'home'}>
|
||
<header className="home__top">
|
||
<div className="home__brand-row">
|
||
<img src="/logo.png" alt="" className="home__mark" />
|
||
<span className="brand brand--sm">Vyryi</span>
|
||
</div>
|
||
<div className="balance-panel">
|
||
<span className="balance-panel__label">Баланс</span>
|
||
<strong className="balance-panel__value">{user?.balance ?? '—'}</strong>
|
||
<button type="button" className="btn btn--gold btn--sm" onClick={() => setDepositOpen(true)}>
|
||
Пополнить
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{message && <p className="toast">{message}</p>}
|
||
|
||
<section className="games" aria-labelledby="games-title">
|
||
<h2 id="games-title">Слоты</h2>
|
||
<p className="section-lead">Лесная Удача уже открыта — остальные ждут своего часа.</p>
|
||
<ul className="games__list">
|
||
{games.map((game) => (
|
||
<li key={game.slug}>
|
||
<Link to={`/games/${game.slug}`} className="game-link">
|
||
<span className="game-link__title">{game.title}</span>
|
||
<span className="game-link__status">
|
||
{game.status === 'available' ? 'Открыто' : 'Скоро'}
|
||
</span>
|
||
<span className="game-link__desc">{game.description}</span>
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
|
||
{depositOpen && (
|
||
<div className="modal" role="dialog" aria-modal="true" aria-labelledby="deposit-title">
|
||
<div className="modal__panel">
|
||
<h3 id="deposit-title">Пополнение</h3>
|
||
<p className="section-lead">Тестовая заглушка. Вывод недоступен.</p>
|
||
<div className="deposit-grid">
|
||
{DEPOSIT_AMOUNTS.map((amount) => (
|
||
<button
|
||
key={amount}
|
||
type="button"
|
||
className="btn btn--outline"
|
||
disabled={busy}
|
||
onClick={() => void deposit(amount)}
|
||
>
|
||
+{amount}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button type="button" className="btn btn--ghost" onClick={() => setDepositOpen(false)}>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
)
|
||
}
|