38be7fea96
- Screen.svelte shell: nav bar grows, ad+content+tabbar pinned bottom (mobile feel) - AdBanner.svelte + banner.ts rotator (params, mock long/short, linkify); Header CSS chevron + grow; Menu (bigger CSS hamburger); TabBar + HoldConfirm shared components; user-select:none - Lobby: hide-empty sections, tab order New/Tournaments/Stats, place-based result badges (result.ts) - Settings: Board style > Labels (beginner/classic/none) + prefs plumbing (boardlabels.ts); i18n keys + ru mirror
151 lines
4.1 KiB
Svelte
151 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import Screen from '../components/Screen.svelte';
|
|
import Menu from '../components/Menu.svelte';
|
|
import TabBar from '../components/TabBar.svelte';
|
|
import { app, handleError, showToast } from '../lib/app.svelte';
|
|
import { gateway } from '../lib/gateway';
|
|
import { navigate } from '../lib/router.svelte';
|
|
import { t } from '../lib/i18n/index.svelte';
|
|
import { resultBadge } from '../lib/result';
|
|
import type { GameView } from '../lib/model';
|
|
|
|
let games = $state<GameView[]>([]);
|
|
|
|
async function load() {
|
|
try {
|
|
games = (await gateway.gamesList()).games;
|
|
} catch (e) {
|
|
handleError(e);
|
|
}
|
|
}
|
|
|
|
onMount(load);
|
|
$effect(() => {
|
|
if (app.lastEvent) void load();
|
|
});
|
|
|
|
const myId = $derived(app.session?.userId ?? '');
|
|
const active = $derived(games.filter((g) => g.status === 'active'));
|
|
const finished = $derived(games.filter((g) => g.status !== 'active'));
|
|
|
|
function opponents(g: GameView): string {
|
|
return g.seats
|
|
.filter((s) => s.accountId !== myId)
|
|
.map((s) => s.displayName)
|
|
.join(', ');
|
|
}
|
|
function scoreline(g: GameView): string {
|
|
const me = g.seats.find((s) => s.accountId === myId);
|
|
const opp = g.seats.filter((s) => s.accountId !== myId).map((s) => s.score);
|
|
return `${me?.score ?? 0} : ${opp.join(', ')}`;
|
|
}
|
|
|
|
const menuItems = $derived([
|
|
{ label: t('lobby.profile'), onclick: () => navigate('/profile') },
|
|
{ label: t('lobby.settings'), onclick: () => navigate('/settings') },
|
|
{ label: t('lobby.about'), onclick: () => navigate('/about') },
|
|
]);
|
|
</script>
|
|
|
|
<Screen title={app.profile?.displayName ?? t('app.title')}>
|
|
{#snippet menu()}
|
|
<Menu items={menuItems} />
|
|
{/snippet}
|
|
|
|
<div class="lobby">
|
|
{#each [{ h: 'lobby.activeGames', list: active }, { h: 'lobby.finishedGames', list: finished }] as group (group.h)}
|
|
{#if group.list.length}
|
|
<section>
|
|
<h2>{t(group.h as 'lobby.activeGames')}</h2>
|
|
{#each group.list as g (g.id)}
|
|
{@const b = resultBadge(g, myId)}
|
|
<button class="row" onclick={() => navigate(`/game/${g.id}`)}>
|
|
<span class="info">
|
|
<span class="who">{opponents(g) || '—'}</span>
|
|
<span class="sub">{t(b.key)} · {scoreline(g)}</span>
|
|
</span>
|
|
<span class="emoji">{b.emoji}</span>
|
|
</button>
|
|
{/each}
|
|
</section>
|
|
{/if}
|
|
{/each}
|
|
|
|
{#if !active.length && !finished.length}
|
|
<p class="empty">{t('lobby.noActive')}</p>
|
|
{/if}
|
|
</div>
|
|
|
|
{#snippet tabbar()}
|
|
<TabBar>
|
|
<button class="tab" onclick={() => navigate('/new')}>
|
|
<span class="sq">🎲</span><span class="lbl">{t('lobby.new')}</span>
|
|
</button>
|
|
<button class="tab" onclick={() => showToast(t('lobby.soon'))}>
|
|
<span class="sq">⚔️</span><span class="lbl">{t('lobby.tournaments')}</span>
|
|
</button>
|
|
<button class="tab" onclick={() => showToast(t('lobby.soon'))}>
|
|
<span class="sq">🧮</span><span class="lbl">{t('lobby.stats')}</span>
|
|
</button>
|
|
</TabBar>
|
|
{/snippet}
|
|
</Screen>
|
|
|
|
<style>
|
|
.lobby {
|
|
padding: var(--pad);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
h2 {
|
|
font-size: 0.9rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-muted);
|
|
margin: 0 0 8px;
|
|
}
|
|
.empty {
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
margin: 0;
|
|
}
|
|
.row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 12px 14px;
|
|
margin-bottom: 8px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text);
|
|
border-radius: var(--radius);
|
|
user-select: none;
|
|
}
|
|
.info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
}
|
|
.who {
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.sub {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
}
|
|
.emoji {
|
|
font-size: 1.8rem;
|
|
line-height: 1;
|
|
flex: 0 0 auto;
|
|
}
|
|
</style>
|