Landing v2: icon switchers, ephemeral theme, channel link, drop browser CTA
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 7s
CI / integration (pull_request) Successful in 10s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 7s
CI / integration (pull_request) Successful in 10s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
Owner review-pass rework of the landing page: - Rename the per-language Telegram link build var VITE_TELEGRAM_LINK_EN/_RU -> VITE_TELEGRAM_GAME_CHANNEL_NAME_EN/_RU (it carries a channel username; the landing builds https://t.me/<name> -- the same channels the connector posts to via TELEGRAM_GAME_CHANNEL_ID_*). - Language switcher -> a globe icon dropdown (flags + names), saved + synced to the app prefs. - Theme switcher -> a sun/moon icon toggle, ephemeral (follows the system scheme, no auto, never persisted) -- galaxy-game style. - Drop the "Play in browser" CTA (no standalone-web onboarding yet). Docs: FUNCTIONAL(+ru), PLAN, deploy + ui READMEs.
This commit is contained in:
+103
-89
@@ -1,89 +1,84 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { applyReduceMotion, applyTheme, type ThemePref } from './lib/theme';
|
||||
import { i18n, localeFrom, setLocale, t, type Locale, type MessageKey } from './lib/i18n/index.svelte';
|
||||
import { applyTheme } from './lib/theme';
|
||||
import { i18n, localeFrom, setLocale, t, type Locale } from './lib/i18n/index.svelte';
|
||||
import { loadPrefs, savePrefs, type Prefs } from './lib/session';
|
||||
import { aboutContent } from './lib/aboutContent';
|
||||
import { telegramBotLink } from './lib/landing';
|
||||
import { telegramChannelLink } from './lib/landing';
|
||||
|
||||
// Standalone landing page (Stage 17): the public entry at "/", separate from the game SPA
|
||||
// (served at /app/ and /telegram/). It reuses the app's theme/i18n/prefs leaf modules — but
|
||||
// not the app store — so it stays light (no gateway, auth or live stream).
|
||||
// Standalone landing page (Stage 17), the public entry at "/" (the game SPA lives at /app/ and
|
||||
// /telegram/). It reuses the app's theme/i18n/prefs leaf modules — not the app store — so it
|
||||
// stays light. Theme is EPHEMERAL here (no auto, no persistence): it starts from the system
|
||||
// scheme and the icon toggles light<->dark in memory. Language IS persisted (synced with the app).
|
||||
|
||||
const themes: ThemePref[] = ['auto', 'light', 'dark'];
|
||||
const themeLabel: Record<ThemePref, MessageKey> = {
|
||||
auto: 'settings.themeAuto',
|
||||
light: 'settings.themeLight',
|
||||
dark: 'settings.themeDark',
|
||||
};
|
||||
const locales: Locale[] = ['en', 'ru'];
|
||||
|
||||
let theme = $state<ThemePref>('auto');
|
||||
let theme = $state<'light' | 'dark'>('light');
|
||||
let langOpen = $state(false);
|
||||
let prefs: Partial<Prefs> = {};
|
||||
|
||||
// The away/move clock the random-game copy mentions (backend game.DefaultTurnTimeout = 24h).
|
||||
const about = $derived(aboutContent(i18n.locale, 24));
|
||||
const tgLink = $derived(telegramBotLink(i18n.locale));
|
||||
const about = $derived(aboutContent(i18n.locale, 24)); // 24h = the auto-match move clock
|
||||
const tgLink = $derived(telegramChannelLink(i18n.locale));
|
||||
const locales: { code: Locale; label: string }[] = [
|
||||
{ code: 'en', label: '🇬🇧 English' },
|
||||
{ code: 'ru', label: '🇷🇺 Русский' },
|
||||
];
|
||||
|
||||
function systemTheme(): 'light' | 'dark' {
|
||||
return typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
prefs = await loadPrefs();
|
||||
theme = prefs.theme ?? 'auto';
|
||||
theme = systemTheme();
|
||||
applyTheme(theme);
|
||||
applyReduceMotion(prefs.reduceMotion ?? false);
|
||||
setLocale(prefs.locale ?? localeFrom(typeof navigator !== 'undefined' ? navigator.language : 'en'));
|
||||
});
|
||||
|
||||
function persist(): void {
|
||||
// savePrefs takes the full set, so keep the labels/lines the app may have stored.
|
||||
function toggleTheme(): void {
|
||||
theme = theme === 'light' ? 'dark' : 'light';
|
||||
applyTheme(theme); // ephemeral — deliberately not persisted
|
||||
}
|
||||
|
||||
function chooseLocale(lc: Locale): void {
|
||||
setLocale(lc);
|
||||
langOpen = false;
|
||||
// Persist the language only, keeping the app's other prefs (notably its own persisted theme).
|
||||
void savePrefs({
|
||||
theme,
|
||||
locale: i18n.locale,
|
||||
theme: prefs.theme ?? 'auto',
|
||||
locale: lc,
|
||||
reduceMotion: prefs.reduceMotion ?? false,
|
||||
boardLabels: prefs.boardLabels ?? 'beginner',
|
||||
boardLines: prefs.boardLines ?? false,
|
||||
});
|
||||
}
|
||||
function chooseTheme(th: ThemePref): void {
|
||||
theme = th;
|
||||
applyTheme(th);
|
||||
persist();
|
||||
}
|
||||
function chooseLocale(lc: Locale): void {
|
||||
setLocale(lc);
|
||||
persist();
|
||||
prefs = { ...prefs, locale: lc };
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="landing">
|
||||
<header class="bar">
|
||||
<div class="seg">
|
||||
{#each locales as lc (lc)}
|
||||
<button class="opt" class:active={i18n.locale === lc} onclick={() => chooseLocale(lc)}>
|
||||
{t(lc === 'en' ? 'lang.en' : 'lang.ru')}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="seg">
|
||||
{#each themes as th (th)}
|
||||
<button class="opt" class:active={theme === th} onclick={() => chooseTheme(th)}>
|
||||
{t(themeLabel[th])}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="lang">
|
||||
<button class="icon" aria-label="Language" aria-expanded={langOpen} onclick={() => (langOpen = !langOpen)}>🌐</button>
|
||||
{#if langOpen}
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button class="backdrop" onclick={() => (langOpen = false)}></button>
|
||||
<div class="menu" role="menu">
|
||||
{#each locales as l (l.code)}
|
||||
<button role="menuitem" class:on={i18n.locale === l.code} onclick={() => chooseLocale(l.code)}>{l.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="icon" aria-label="Theme" onclick={toggleTheme}>{theme === 'light' ? '☼' : '☾'}</button>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<h1>{about.title}</h1>
|
||||
<p class="tagline">{t('landing.tagline')}</p>
|
||||
<div class="cta">
|
||||
<a class="play primary" href="/app/">{t('landing.playWeb')}</a>
|
||||
{#if tgLink}
|
||||
<a class="play tg" href={tgLink} target="_blank" rel="noopener noreferrer">
|
||||
<img src="telegram-logo.svg" alt="" width="22" height="22" />
|
||||
{t('landing.playTelegram')}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{#if tgLink}
|
||||
<a class="play" href={tgLink} target="_blank" rel="noopener noreferrer">
|
||||
<img src="telegram-logo.svg" alt="" width="22" height="22" />
|
||||
{t('landing.playTelegram')}
|
||||
</a>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="info">
|
||||
@@ -125,32 +120,65 @@
|
||||
.bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.seg {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
.lang {
|
||||
position: relative;
|
||||
}
|
||||
.opt {
|
||||
padding: 7px 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
.icon {
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
user-select: none;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.opt.active {
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
border-color: var(--accent);
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 8;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
.menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 9;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.menu button {
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.menu button:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.menu button.on {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.hero {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 16px;
|
||||
padding: 24px 0 8px;
|
||||
}
|
||||
.hero h1 {
|
||||
@@ -164,33 +192,20 @@
|
||||
color: var(--text-muted);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.cta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.play {
|
||||
align-self: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 12px 22px;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.play.primary {
|
||||
background: var(--accent);
|
||||
color: var(--accent-text);
|
||||
border-color: var(--accent);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.play.tg {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.play.tg img {
|
||||
.play img {
|
||||
display: block;
|
||||
}
|
||||
.info {
|
||||
@@ -225,7 +240,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
color: var(--text);
|
||||
}
|
||||
.ft {
|
||||
margin-top: auto;
|
||||
|
||||
Reference in New Issue
Block a user