diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 97059c7..909d159 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -335,6 +335,7 @@ jobs: VITE_TELEGRAM_BOT_ID: ${{ vars.TEST_VITE_TELEGRAM_BOT_ID }} VITE_TELEGRAM_LINK: ${{ vars.TEST_VITE_TELEGRAM_LINK }} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${{ vars.TEST_VITE_TELEGRAM_GAME_CHANNEL_NAME }} + VITE_VK_APP_LINK: ${{ vars.TEST_VITE_VK_APP_LINK }} VITE_GATEWAY_URL: ${{ vars.TEST_VITE_GATEWAY_URL }} # Unset vars render empty -> the compose ":-" defaults apply. POSTGRES_DB: ${{ vars.TEST_POSTGRES_DB }} diff --git a/.gitea/workflows/prod-deploy.yaml b/.gitea/workflows/prod-deploy.yaml index 99602d0..af0fcbd 100644 --- a/.gitea/workflows/prod-deploy.yaml +++ b/.gitea/workflows/prod-deploy.yaml @@ -40,6 +40,7 @@ jobs: VITE_TELEGRAM_BOT_ID: ${{ vars.PROD_VITE_TELEGRAM_BOT_ID }} VITE_TELEGRAM_LINK: ${{ vars.PROD_VITE_TELEGRAM_LINK }} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${{ vars.PROD_VITE_TELEGRAM_GAME_CHANNEL_NAME }} + VITE_VK_APP_LINK: ${{ vars.PROD_VITE_VK_APP_LINK }} VITE_GATEWAY_URL: ${{ vars.PROD_VITE_GATEWAY_URL }} POSTGRES_PASSWORD: ${{ secrets.PROD_POSTGRES_PASSWORD }} GM_BASICAUTH_HASH: ${{ secrets.PROD_GM_BASICAUTH_HASH }} diff --git a/deploy/.env.example b/deploy/.env.example index acd067f..376f154 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -32,6 +32,7 @@ GM_BASICAUTH_HASH= # required; `caddy hash-password` bcrypt VITE_TELEGRAM_BOT_ID= VITE_TELEGRAM_LINK= # friend-invite Mini App link (full URL, e.g. https://t.me//) VITE_TELEGRAM_GAME_CHANNEL_NAME= # landing "Play in Telegram" link, the bot's game channel +VITE_VK_APP_LINK= # landing "Play on VK" link (full URL, https://vk.com/app) VITE_GATEWAY_URL= # --- Grafana ---------------------------------------------------------------- diff --git a/deploy/README.md b/deploy/README.md index f8b4a12..e4e463a 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -92,9 +92,10 @@ without it Docker's resolver handles `otelcol`, `gateway` and `api.telegram.org` | `VITE_TELEGRAM_BOT_ID` | variable | _(empty)_ | UI build-arg: numeric bot id for the web Login Widget. | | `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: friend-invite Mini App link (full URL, `https://t.me//` — `` is the Mini App short name from BotFather). | | `VITE_TELEGRAM_GAME_CHANNEL_NAME` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link, the bot's game channel (e.g. `https://t.me/Erudit_Game`). | +| `VITE_VK_APP_LINK` | variable | _(empty)_ | UI build-arg: the landing "Play on VK" link, the VK Mini App (full URL, `https://vk.com/app`). | | `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). | -The five `VITE_*` are **build-args** baked into the gateway and landing images at +The six `VITE_*` are **build-args** baked into the gateway and landing images at build time (both targets share one UI build stage — keep the args identical so it is built once), so changing them requires a rebuild (`--build`), not just a restart. @@ -198,7 +199,7 @@ BOTLINK_GATEWAY_CERT, BOTLINK_GATEWAY_KEY, BOTLINK_BOT_CERT, BOTLINK_BOT_KEY}`; `PROD_{REGISTRY_USER, MAIN_HOST, TG_HOST, CADDY_SITE_ADDRESS, GM_BASICAUTH_USER, GRAFANA_ROOT_URL, LOG_LEVEL, DICT_VERSION, TELEGRAM_MINIAPP_URL, TELEGRAM_GAME_CHANNEL_ID, TELEGRAM_CHAT_ID, TELEGRAM_BOT_USERNAME, VITE_TELEGRAM_BOT_ID, VITE_TELEGRAM_LINK, -VITE_TELEGRAM_GAME_CHANNEL_NAME}`. +VITE_TELEGRAM_GAME_CHANNEL_NAME, VITE_VK_APP_LINK}`. ## Host-side setup (outside this repo) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 34893cb..5ea740a 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -134,6 +134,7 @@ services: VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-} VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-} + VITE_VK_APP_LINK: ${VITE_VK_APP_LINK:-} VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-} VITE_APP_VERSION: ${APP_VERSION:-dev} # Go binary version (the SPA's VITE_APP_VERSION is the same git tag). @@ -207,6 +208,7 @@ services: VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-} VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-} + VITE_VK_APP_LINK: ${VITE_VK_APP_LINK:-} VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-} VITE_APP_VERSION: ${APP_VERSION:-dev} restart: unless-stopped diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index e05d9d1..be03167 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -21,7 +21,7 @@ Settings also pick the board's bonus-label style (beginner / classic / none). A costs nothing when the rack has no legal move. The word-check accepts only the variant's alphabet, remembers answers within the session and rate-limits repeats. A public **landing page** at the site root introduces the game, switches language and -theme, and links to the matching per-language Telegram channel; the game itself runs at +theme, and links to the Telegram game channel and the VK Mini App; the game itself runs at `/app/` (web), `/telegram/` (the Telegram Mini App) and `/vk/` (the VK Mini App). The landing's theme is ephemeral (it follows the system scheme, not the saved preference); its language choice is saved. diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index f0d1aca..887eaf7 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -22,7 +22,7 @@ top-1 подсказку, безлимитную проверку слова с Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии и ограничивает частоту повторов. Публичная **посадочная страница** в корне сайта представляет игру, переключает язык и -тему и ведёт в соответствующий по-язычный Telegram-канал; сама игра живёт по адресам +тему и ведёт в Telegram-канал игры и в VK Mini App; сама игра живёт по адресам `/app/` (веб), `/telegram/` (Telegram Mini App) и `/vk/` (VK Mini App). Тема на странице эфемерна (берётся из системной настройки, а не из сохранённой), выбор языка сохраняется. diff --git a/gateway/Dockerfile b/gateway/Dockerfile index c8e5bf9..3cb62cc 100644 --- a/gateway/Dockerfile +++ b/gateway/Dockerfile @@ -23,18 +23,14 @@ RUN corepack enable && corepack prepare pnpm@11.0.9 --activate # VITE_APP_VERSION carries `git describe` for the About screen (defaults to "dev"). ARG VITE_TELEGRAM_BOT_ID= ARG VITE_TELEGRAM_LINK= -ARG VITE_TELEGRAM_LINK_EN= -ARG VITE_TELEGRAM_LINK_RU= -ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_EN= -ARG VITE_TELEGRAM_GAME_CHANNEL_NAME_RU= +ARG VITE_TELEGRAM_GAME_CHANNEL_NAME= +ARG VITE_VK_APP_LINK= ARG VITE_GATEWAY_URL= ARG VITE_APP_VERSION= ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \ VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \ - VITE_TELEGRAM_LINK_EN=$VITE_TELEGRAM_LINK_EN \ - VITE_TELEGRAM_LINK_RU=$VITE_TELEGRAM_LINK_RU \ - VITE_TELEGRAM_GAME_CHANNEL_NAME_EN=$VITE_TELEGRAM_GAME_CHANNEL_NAME_EN \ - VITE_TELEGRAM_GAME_CHANNEL_NAME_RU=$VITE_TELEGRAM_GAME_CHANNEL_NAME_RU \ + VITE_TELEGRAM_GAME_CHANNEL_NAME=$VITE_TELEGRAM_GAME_CHANNEL_NAME \ + VITE_VK_APP_LINK=$VITE_VK_APP_LINK \ VITE_GATEWAY_URL=$VITE_GATEWAY_URL \ VITE_APP_VERSION=$VITE_APP_VERSION diff --git a/ui/public/vk-logo.svg b/ui/public/vk-logo.svg new file mode 100644 index 0000000..1c61468 --- /dev/null +++ b/ui/public/vk-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/src/Landing.svelte b/ui/src/Landing.svelte index e135e8b..f9d11a9 100644 --- a/ui/src/Landing.svelte +++ b/ui/src/Landing.svelte @@ -4,7 +4,7 @@ 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 { telegramChannelLink } from './lib/landing'; + import { telegramChannelLink, vkAppLink } from './lib/landing'; // Standalone landing page, 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 @@ -17,6 +17,7 @@ const about = $derived(aboutContent(i18n.locale, 24)); // 24h = the auto-match move clock const tgLink = $derived(telegramChannelLink()); + const vkLink = $derived(vkAppLink()); const locales: { code: Locale; label: string }[] = [ { code: 'en', label: '🇬🇧 English' }, { code: 'ru', label: '🇷🇺 Русский' }, @@ -72,10 +73,19 @@

{about.title}

{t('landing.tagline')}

- {#if tgLink} - - - + {#if tgLink || vkLink} +
+ {#if tgLink} + + + + {/if} + {#if vkLink} + + + + {/if} +
{/if}
@@ -190,19 +200,22 @@ color: var(--text-muted); font-size: 1.05rem; } - /* The Telegram entry is just the bigger logo (no button chrome, no caption); the link - keeps an aria-label for assistive tech. */ - .tg { + /* The platform entries are just the bigger logos in a row (no button chrome, no captions); + each link keeps an aria-label for assistive tech. */ + .entries { align-self: center; - display: inline-flex; + display: flex; + gap: 20px; margin-top: 8px; - border-radius: 50%; } - .tg img { + .entry { + display: inline-flex; + } + .entry img { display: block; transition: transform 0.12s ease; } - .tg:hover img { + .entry:hover img { transform: scale(1.06); } .info { diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index 2254855..3e4a125 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -205,6 +205,7 @@ export const en = { 'landing.tagline': 'Play Scrabble with friends or a smart robot — in your browser or on Telegram.', 'landing.playTelegram': 'Play in Telegram', + 'landing.playVK': 'Play on VK', 'lang.en': 'English', 'lang.ru': 'Русский', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index 53fba60..f698955 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -205,6 +205,7 @@ export const ru: Record = { 'landing.tagline': 'Играй в Скрэббл с друзьями или умным роботом — в браузере или в Telegram.', 'landing.playTelegram': 'Играть в Telegram', + 'landing.playVK': 'Играть во ВКонтакте', 'lang.en': 'English', 'lang.ru': 'Русский', diff --git a/ui/src/lib/landing.test.ts b/ui/src/lib/landing.test.ts index 71a9c56..803ff14 100644 --- a/ui/src/lib/landing.test.ts +++ b/ui/src/lib/landing.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { telegramChannelLink } from './landing'; +import { telegramChannelLink, vkAppLink } from './landing'; describe('telegramChannelLink', () => { afterEach(() => vi.unstubAllEnvs()); @@ -14,3 +14,17 @@ describe('telegramChannelLink', () => { expect(telegramChannelLink()).toBeNull(); }); }); + +describe('vkAppLink', () => { + afterEach(() => vi.unstubAllEnvs()); + + it('returns the configured VK app link verbatim', () => { + vi.stubEnv('VITE_VK_APP_LINK', 'https://vk.com/app54655464'); + expect(vkAppLink()).toBe('https://vk.com/app54655464'); + }); + + it('returns null when the link is unset or blank', () => { + vi.stubEnv('VITE_VK_APP_LINK', ' '); + expect(vkAppLink()).toBeNull(); + }); +}); diff --git a/ui/src/lib/landing.ts b/ui/src/lib/landing.ts index 7d5ed43..2943705 100644 --- a/ui/src/lib/landing.ts +++ b/ui/src/lib/landing.ts @@ -1,5 +1,5 @@ // Pure helpers for the public landing page, kept out of the Svelte component so the -// Telegram-channel link selection is unit-testable. +// platform-entry link selection is unit-testable. /** * telegramChannelLink returns the t.me link for the single bot's game channel, or null @@ -13,3 +13,15 @@ export function telegramChannelLink(): string | null { const name = (raw as string | undefined)?.trim().replace(/^@/, ''); return name ? `https://t.me/${name}` : null; } + +/** + * vkAppLink returns the VK Mini App link for the landing "Play on VK" entry, or null when it is + * not configured. The link is a build-time var (VITE_VK_APP_LINK, the full URL + * `https://vk.com/app`) because the test and prod contours run the same VK app but the var + * mirrors the per-contour Telegram entries. + */ +export function vkAppLink(): string | null { + const raw = import.meta.env.VITE_VK_APP_LINK; + const link = (raw as string | undefined)?.trim(); + return link ? link : null; +}