From c8a6c77a8e499c61465fc71a890dbba26f6be669 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Wed, 10 Jun 2026 22:03:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(landing):=20rework=20=E2=80=94=20es=20defa?= =?UTF-8?q?ult,=202-step=20join,=20roster=20+=20account=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default language Spanish at root /, English moves to /en/ (eu unchanged): getStaticPaths {lang:undefined}->es + LANG_*_PATH maps swapped in ui.ts. - Section order: hero -> join -> status -> members -> mods -> features. - Join flow 3 -> 2 steps (drop the Connect/IP step; launcher handles the address). Remove the hero IP CopyChip. Header CTA renamed Play/Jugar/Jolastu and gains Register + Login links on all four pages. - Account page: hide the whole login block once authenticated (not just the form), add an avatar Refresh button (cache-busted NMSR re-fetch), flatten the avatar (no bevel). New i18n key account.refresh. - Register page: new REGISTRATION_SHOW_INVITE env (default false/hidden) toggles the invite-code field independently of the REGISTRATION_MODE backend gate. - Members roster: drop the `admin` account, render full-body via a new ROSTER_AVATAR_MODE (default fullbodyiso); portrait tiles. - Style: flatten emboss on feature icons (.picon) + player tiles (.roster-tile). PixelIcon gains optional image support (/icons/.png|svg). - Favicon now /favicon.png (committed) on all pages. - Docs: plan/09-landing.md Assets section (logo/favicon/features-icons drop points) + default-language note; .env.example documents the two new vars. Co-Authored-By: Claude Opus 4.8 --- .env.example | 21 ++- landing/public/favicon.png | Bin 0 -> 9253 bytes landing/src/components/PixelIcon.astro | 17 +- landing/src/components/PlayerRoster.astro | 11 +- landing/src/data/site.ts | 28 +++- landing/src/i18n/ui.ts | 70 +++----- landing/src/pages/[...lang].astro | 182 ++++++++++----------- landing/src/pages/[...lang]/account.astro | 64 +++++--- landing/src/pages/[...lang]/fjord.astro | 12 +- landing/src/pages/[...lang]/register.astro | 20 ++- landing/src/styles/main.css | 15 +- plan/09-landing.md | 34 +++- 12 files changed, 267 insertions(+), 207 deletions(-) create mode 100644 landing/public/favicon.png diff --git a/.env.example b/.env.example index 8fce978..8204ee6 100644 --- a/.env.example +++ b/.env.example @@ -7,18 +7,29 @@ BASE_DOMAIN=ulicraft.net RCON_PASSWORD=change-me-to-something-random -# Registration mode for self-hosted accounts (Drasl) + landing register form. +# Registration mode (BACKEND gate) for self-hosted accounts (Drasl). # invite = closed; guests need an admin-minted invite code (recommended on a # public, internet-present host). Admin mints via POST /drasl/api/v2/invites. # open = anyone can self-register with username+password (set [RateLimit]!). -# Consumed by BOTH render-config.sh (-> Drasl RegistrationNewPlayer.RequireInvite) -# and the landing build (-> show/hide the invite-code field). Default: invite. +# Consumed by render-config.sh (-> Drasl RegistrationNewPlayer.RequireInvite). +# Default: invite. NOTE: this no longer controls the landing invite FIELD — that's +# REGISTRATION_SHOW_INVITE below. REGISTRATION_MODE=invite -# NMSR avatar render mode for the landing's avatar tiles (online players + -# Members roster). Read by the landing BUILD (data/site.ts) only. Avatar URL = +# Show the invite-code FIELD on the landing register form. Read by the landing +# BUILD (data/site.ts) only; default false (hidden). Independent of the backend +# gate above. CAVEAT: if REGISTRATION_MODE=invite (Drasl requires a code) but this +# is false, public self-registration can't supply one — enable both together, or +# mint accounts admin-side. true | false. +REGISTRATION_SHOW_INVITE=false + +# NMSR avatar render mode for the landing's avatar tiles. Read by the landing +# BUILD (data/site.ts) only. Avatar URL = # https://avatar.${BASE_DOMAIN}//?size=N. e.g. headiso | head | fullbody. +# AVATAR_MODE → ServerCard online row + account preview (heads) +# ROSTER_AVATAR_MODE → Members grid (full body; default fullbodyiso) AVATAR_MODE=headiso +ROSTER_AVATAR_MODE=fullbodyiso # Drasl admin creds for the registered-players roster (/api/mcstatus/players). # Consumed ONLY by the mc-status container (server-side login → sanitized diff --git a/landing/public/favicon.png b/landing/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..88fda3282b9c2b185f8d847e65660bd3e5924711 GIT binary patch literal 9253 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 = { shield: ["0111110", "1111111", "1111111", "1111111", "0111110", "0011100", "0001000"], @@ -15,6 +18,10 @@ const GLYPHS: Record = { const cells = GLYPHS[glyph].join("").split(""); const onClass = gold ? "go" : "on"; --- - +{img ? ( + +) : ( + +)} diff --git a/landing/src/components/PlayerRoster.astro b/landing/src/components/PlayerRoster.astro index 82bbe93..8708a4d 100644 --- a/landing/src/components/PlayerRoster.astro +++ b/landing/src/components/PlayerRoster.astro @@ -19,13 +19,13 @@ interface Props { } const { emptyLabel = "No members yet." } = Astro.props; -const { status, avatarUrl, avatarMode } = site; +const { status, avatarUrl, rosterAvatarMode } = site; ---
@@ -58,7 +58,10 @@ const { status, avatarUrl, avatarMode } = site; return showEmpty(); } - const players = Array.isArray(data) ? data.filter((p) => p && p.uuid) : []; + // Drop the service `admin` account from the public members grid. + const players = Array.isArray(data) + ? data.filter((p) => p && p.uuid && p.name?.toLowerCase() !== "admin") + : []; if (players.length === 0) return showEmpty(); grid.innerHTML = ""; @@ -75,7 +78,7 @@ const { status, avatarUrl, avatarMode } = site; const img = document.createElement("img"); img.loading = "lazy"; img.alt = name; - img.src = `${avatar}/${mode}/${uuid}?size=64`; + img.src = `${avatar}/${mode}/${uuid}?size=128`; const label = document.createElement("span"); label.className = "roster-name"; label.textContent = name; diff --git a/landing/src/data/site.ts b/landing/src/data/site.ts index b801376..2111aad 100644 --- a/landing/src/data/site.ts +++ b/landing/src/data/site.ts @@ -14,10 +14,14 @@ const BASE_DOMAIN = process.env.BASE_DOMAIN ?? "ulicraft.local"; // is absent/empty (fresh checkout), fall back to the honest "50+" estimate. const MOD_COUNT_KEY = MODS.count > 0 ? `${MODS.count}` : "50+"; -// NMSR render mode for avatar tiles (online roster + members grid). Read at -// build time, same as BASE_DOMAIN. e.g. "headiso" | "head" | "fullbody". +// NMSR render mode for avatar tiles. AVATAR_MODE is the ServerCard online row + +// account preview (heads); ROSTER_AVATAR_MODE is the Members grid (full body by +// default). Read at build time, same as BASE_DOMAIN. e.g. "headiso" | "head" | +// "fullbody" | "fullbodyiso". // @ts-ignore — `process` is untyped here (no @types/node), same as BASE_DOMAIN. const AVATAR_MODE = process.env.AVATAR_MODE ?? "headiso"; +// @ts-ignore — `process` is untyped here (no @types/node). +const ROSTER_AVATAR_MODE = process.env.ROSTER_AVATAR_MODE ?? "fullbodyiso"; // Launcher download manifest (per launcher-manifest.schema.json). The // custom-launcher release flow syncs launcher-manifest.json into this dir @@ -88,6 +92,15 @@ const LAUNCHER_MANIFEST = loadLauncherManifest(); // render-config.sh. "invite" => the register form must collect an invite code. const REGISTRATION_MODE = process.env.REGISTRATION_MODE ?? "invite"; +// REGISTRATION_SHOW_INVITE (true|false, default false=hidden) ONLY toggles the +// visibility of the invite-code field on the register form. The backend invite +// gate stays on Drasl's RequireInvite (driven by REGISTRATION_MODE). Decoupled on +// purpose: invites are usually admin-minted and the field is hidden by default. +// CAVEAT: if REGISTRATION_MODE=invite (backend requires a code) but this is +// false, public self-registration can't supply one — set both together. +// @ts-ignore — `process` is untyped here (no @types/node). +const REGISTRATION_SHOW_INVITE = process.env.REGISTRATION_SHOW_INVITE ?? "false"; + // Minecraft / pack facts, surfaced in a few places. const MC_VERSION = "1.21.1"; @@ -97,9 +110,12 @@ export const site = { mcVersion: MC_VERSION, // Register form behaviour, driven by REGISTRATION_MODE in .env. - // true => show the invite-code field (Drasl RequireInvite = true) + // true => Drasl RequireInvite = true (backend gate) // false => open self-registration registrationRequiresInvite: REGISTRATION_MODE === "invite", + // Whether the invite-code FIELD is shown on the register form (default false). + // Independent of the backend gate above — see REGISTRATION_SHOW_INVITE note. + registrationShowInvite: REGISTRATION_SHOW_INVITE === "true", // Design knobs (replace the design's in-browser TweaksPanel). Baked at build. // mood: "grass" | "nether" | "end" @@ -127,9 +143,11 @@ export const site = { // Mojang/Crafatar — head URL is `${avatarUrl}/headiso/?size=64`. avatarUrl: `https://avatar.${BASE_DOMAIN}`, - // NMSR render mode for avatar tiles (ServerCard online row + PlayerRoster - // members grid). Avatar URL = `${avatarUrl}/${avatarMode}/?size=N`. + // NMSR render mode for avatar tiles. Avatar URL = `${avatarUrl}//?size=N`. + // avatarMode → ServerCard online row + account preview (heads) + // rosterAvatarMode → PlayerRoster members grid (full body) avatarMode: AVATAR_MODE, + rosterAvatarMode: ROSTER_AVATAR_MODE, // Uptime Kuma status page (external link in footer). statusUrl: `https://status.${BASE_DOMAIN}`, diff --git a/landing/src/i18n/ui.ts b/landing/src/i18n/ui.ts index c4d2b90..76f0708 100644 --- a/landing/src/i18n/ui.ts +++ b/landing/src/i18n/ui.ts @@ -11,31 +11,31 @@ export type Lang = "en" | "es" | "eu"; export const LANGS: Lang[] = ["en", "es", "eu"]; export const LANG_LABEL: Record = { en: "EN", es: "ES", eu: "EU" }; -// URL path per locale (en is default, served at root). -export const LANG_PATH: Record = { en: "/", es: "/es/", eu: "/eu/" }; -// Register page path per locale (mirrors LANG_PATH: en at /register). +// URL path per locale (es is default, served at root; en moves to /en/). +export const LANG_PATH: Record = { es: "/", en: "/en/", eu: "/eu/" }; +// Register page path per locale (mirrors LANG_PATH: es at /register). export const LANG_REGISTER_PATH: Record = { - en: "/register", - es: "/es/register", + es: "/register", + en: "/en/register", eu: "/eu/register", }; // Fjord (alternative path) page path per locale (mirrors LANG_REGISTER_PATH). export const LANG_FJORD_PATH: Record = { - en: "/fjord", - es: "/es/fjord", + es: "/fjord", + en: "/en/fjord", eu: "/eu/fjord", }; // Account page path per locale (mirrors LANG_REGISTER_PATH). export const LANG_ACCOUNT_PATH: Record = { - en: "/account", - es: "/es/account", + es: "/account", + en: "/en/account", eu: "/eu/account", }; export interface UI { htmlLang: string; copied: string; // transient copy-button feedback - nav: { status: string; members: string; features: string; mods: string; join: string; play: string }; + nav: { status: string; members: string; features: string; mods: string; join: string; play: string; register: string; login: string }; hero: { eyebrow: string; // "Modded LAN · NeoForge" — version appended at render tagline: string; @@ -84,13 +84,6 @@ export interface UI { registerLink: string; }; s2: { kicker: string; h: string; p: string }; - s3: { - kicker: string; - h: string; - pa: string; // before the Multiplayer/Add Server kbds - pb: string; // after them - copy: string; - }; }; fjord: { navHome: string; // "← Home" link in nav @@ -164,6 +157,7 @@ export interface UI { noAccount: string; // "No account yet?" registerLink: string; // links to /register // Skin panel + refresh: string; // re-fetch the NMSR avatar playerLabel: string; // "Logged in as" skinTitle: string; skinLead: string; // reuses register.skinLead concept @@ -189,7 +183,7 @@ export const ui: Record = { en: { htmlLang: "en", copied: "Copied!", - nav: { status: "Status", members: "Members", features: "Features", mods: "Mods", join: "How to Join", play: "Play Now" }, + nav: { status: "Status", members: "Members", features: "Features", mods: "Mods", join: "How to Join", play: "Play", register: "Register", login: "Log in" }, hero: { eyebrow: "Modded LAN · NeoForge", tagline: "Kitchen-sink survival, built to outlast the weekend.", @@ -249,7 +243,7 @@ export const ui: Record = { empty: "Mod list not generated yet — check back soon.", }, join: { - eyebrow: "How to Join · 3 Steps", + eyebrow: "How to Join · 2 Steps", title: "From zero to spawning in.", lead: "One-time setup. After this, the launcher keeps you in sync.", s1: { @@ -262,14 +256,7 @@ export const ui: Record = { s2: { kicker: "Launcher", h: "Download {name}", - p: "Grab {name} for your system and log in with your Ulicraft credentials — it installs the modpack and everything else automatically.", - }, - s3: { - kicker: "Connect", - h: "Join the server", - pa: "Launch the launcher, open", - pb: ", paste the address, and join.", - copy: "Copy IP", + p: "Grab {name}, log in with your Ulicraft credentials, and hit play. Everything else — the modpack, the server address, updates — is handled inside the launcher.", }, }, fjord: { @@ -354,6 +341,7 @@ export const ui: Record = { submitting: "Logging in…", noAccount: "No account yet?", registerLink: "Register here", + refresh: "Refresh", playerLabel: "Logged in as", skinTitle: "Your skin", skinLead: "Upload a Minecraft skin PNG. The 3D preview below updates instantly; the in-game avatar takes ~1 minute.", @@ -377,7 +365,7 @@ export const ui: Record = { es: { htmlLang: "es", copied: "¡Copiado!", - nav: { status: "Estado", members: "Miembros", features: "Características", mods: "Mods", join: "Cómo entrar", play: "Jugar ahora" }, + nav: { status: "Estado", members: "Miembros", features: "Características", mods: "Mods", join: "Cómo entrar", play: "Jugar", register: "Registro", login: "Entrar" }, hero: { eyebrow: "LAN con mods · NeoForge", tagline: "Supervivencia todo incluido, para durar más que el finde.", @@ -437,7 +425,7 @@ export const ui: Record = { empty: "La lista de mods aún no está generada — vuelve pronto.", }, join: { - eyebrow: "Cómo entrar · 3 pasos", + eyebrow: "Cómo entrar · 2 pasos", title: "De cero a aparecer en el mundo.", lead: "Configuración única. Después, el launcher te mantiene sincronizado.", s1: { @@ -450,14 +438,7 @@ export const ui: Record = { s2: { kicker: "Launcher", h: "Descarga {name}", - p: "Coge {name} para tu sistema e inicia sesión con tus credenciales de Ulicraft: instala el modpack y todo lo demás automáticamente.", - }, - s3: { - kicker: "Conectar", - h: "Entra al servidor", - pa: "Lanza el launcher, abre", - pb: ", pega la dirección y entra.", - copy: "Copiar IP", + p: "Coge {name}, inicia sesión con tus credenciales de Ulicraft y dale a jugar. Todo lo demás — el modpack, la dirección del servidor, las actualizaciones — se gestiona dentro del launcher.", }, }, fjord: { @@ -542,6 +523,7 @@ export const ui: Record = { submitting: "Entrando…", noAccount: "¿Aún no tienes cuenta?", registerLink: "Regístrate aquí", + refresh: "Actualizar", playerLabel: "Sesión iniciada como", skinTitle: "Tu skin", skinLead: "Sube un PNG de skin de Minecraft. La vista previa 3D se actualiza al instante; el avatar en el juego tarda ~1 minuto.", @@ -565,7 +547,7 @@ export const ui: Record = { eu: { htmlLang: "eu", copied: "Kopiatuta!", - nav: { status: "Egoera", members: "Kideak", features: "Ezaugarriak", mods: "Mods", join: "Nola sartu", play: "Jokatu orain" }, + nav: { status: "Egoera", members: "Kideak", features: "Ezaugarriak", mods: "Mods", join: "Nola sartu", play: "Jolastu", register: "Erregistroa", login: "Sartu" }, hero: { eyebrow: "Mod-dun LANa · NeoForge", tagline: "Mod ugariko biziraupena, irauteko egina.", @@ -625,7 +607,7 @@ export const ui: Record = { empty: "Mod zerrenda oraindik ez da sortu — itzuli laster.", }, join: { - eyebrow: "Nola sartu · 3 urrats", + eyebrow: "Nola sartu · 2 urrats", title: "Zerotik mundura agertzera.", lead: "Behin bakarrik konfiguratu. Gero, launcherrak sinkronizatuta mantenduko zaitu.", s1: { @@ -638,14 +620,7 @@ export const ui: Record = { s2: { kicker: "Launcherra", h: "Deskargatu {name}", - p: "Hartu {name} zure sistemarako eta hasi saioa zure Ulicraft kredentzialekin: modpacka eta gainerako guztia automatikoki instalatzen du.", - }, - s3: { - kicker: "Konektatu", - h: "Sartu zerbitzarira", - pa: "Abiarazi launcherra, ireki", - pb: ", itsatsi helbidea eta sartu.", - copy: "Kopiatu IPa", + p: "Hartu {name}, hasi saioa zure Ulicraft kredentzialekin eta sakatu jolastu. Gainerako guztia — modpacka, zerbitzariaren helbidea, eguneraketak — launcherraren barruan kudeatzen da.", }, }, fjord: { @@ -730,6 +705,7 @@ export const ui: Record = { submitting: "Sartzen…", noAccount: "Oraindik konturik ez?", registerLink: "Erregistratu hemen", + refresh: "Eguneratu", playerLabel: "Saioa hasita:", skinTitle: "Zure azala", skinLead: "Igo Minecraft azal PNG bat. 3D aurrebista berehala eguneratzen da; jokoko avatarra ~1 minutuan.", diff --git a/landing/src/pages/[...lang].astro b/landing/src/pages/[...lang].astro index f57e708..542f6dd 100644 --- a/landing/src/pages/[...lang].astro +++ b/landing/src/pages/[...lang].astro @@ -1,9 +1,8 @@ --- -import { site, LITERALS, HEAD_FONTS } from "../data/site"; -import { ui, LANGS, LANG_LABEL, LANG_PATH, LANG_REGISTER_PATH, type Lang } from "../i18n/ui"; +import { site, HEAD_FONTS } from "../data/site"; +import { ui, LANGS, LANG_LABEL, LANG_PATH, LANG_REGISTER_PATH, LANG_ACCOUNT_PATH, type Lang } from "../i18n/ui"; import Creeper from "../components/Creeper.astro"; import PixelIcon from "../components/PixelIcon.astro"; -import CopyChip from "../components/CopyChip.astro"; import ServerCard from "../components/ServerCard.astro"; import PlayerRoster from "../components/PlayerRoster.astro"; import ModList from "../components/ModList.astro"; @@ -12,8 +11,8 @@ import "../styles/main.css"; // One static page per locale: en at "/", es at "/es/", eu at "/eu/". export function getStaticPaths() { return [ - { params: { lang: undefined }, props: { locale: "en" as Lang } }, - { params: { lang: "es" }, props: { locale: "es" as Lang } }, + { params: { lang: undefined }, props: { locale: "es" as Lang } }, + { params: { lang: "en" }, props: { locale: "en" as Lang } }, { params: { lang: "eu" }, props: { locale: "eu" as Lang } }, ]; } @@ -24,6 +23,7 @@ const { theme, launcherManifest } = site; const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify; const launcherName = launcherManifest.product; const registerPath = LANG_REGISTER_PATH[locale]; +const accountPath = LANG_ACCOUNT_PATH[locale]; // Per-OS download buttons (join step 2) from the synced manifest. Missing // manifest → empty builds → no buttons (the build never fails on it). @@ -60,7 +60,7 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { {site.name} — {t.hero.howToJoin} - + {LANGS.map((l) => )} @@ -73,11 +73,11 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { ULICRAFT
@@ -85,6 +85,8 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { {LANG_LABEL[l]} ))}
+ {t.nav.register} + {t.nav.login} {t.nav.play}
@@ -110,8 +112,7 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {

{t.hero.tagline}

{t.hero.sub}

Java {site.mcVersion} @@ -131,82 +132,6 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
- -
-
-
- {t.status.eyebrow} -

{t.status.title}

-

{t.status.lead}

-
-
- -
-
- {site.stats.map((s, i) => ( -
-
{s.key}
-
{t.statLabels[i]}
-
- ))} -
-
-
- - -
-
-
- {t.members.eyebrow} -

{t.members.title}

-

{t.members.lead}

-
-
- -
-
-
- - -
-
-
- {t.features.eyebrow} -

{t.features.title}

-

{t.features.lead}

-
-
- {site.features.map((f, i) => ( -
- -
-

{t.features.items[i].h}

-

{t.features.items[i].p}

-
-
- ))} -
-
-
- - -
-
-
- {t.mods.eyebrow} -

{t.mods.title}

-

{t.mods.lead}

-
-
- -
-
-
-
@@ -247,21 +172,82 @@ const dustBits = Array.from({ length: 16 }, (_, i) => { )}
+ + +
- -
-
3
-
- {t.join.s3.kicker} -

{t.join.s3.h}

-

- {t.join.s3.pa} {LITERALS.multiplayer}{LITERALS.addServer}{t.join.s3.pb} -

-
- + +
+
+
+ {t.status.eyebrow} +

{t.status.title}

+

{t.status.lead}

+
+
+ +
+
+ {site.stats.map((s, i) => ( +
+
{s.key}
+
{t.statLabels[i]}
+
+ ))} +
+
+
+ + +
+
+
+ {t.members.eyebrow} +

{t.members.title}

+

{t.members.lead}

+
+
+ +
+
+
+ + +
+
+
+ {t.mods.eyebrow} +

{t.mods.title}

+

{t.mods.lead}

+
+
+ +
+
+
+ + +
+
+
+ {t.features.eyebrow} +

{t.features.title}

+

{t.features.lead}

+
+
+ {site.features.map((f, i) => ( +
+ +
+

{t.features.items[i].h}

+

{t.features.items[i].p}

-
+ ))}
diff --git a/landing/src/pages/[...lang]/account.astro b/landing/src/pages/[...lang]/account.astro index 46c87d5..d7f73a0 100644 --- a/landing/src/pages/[...lang]/account.astro +++ b/landing/src/pages/[...lang]/account.astro @@ -15,8 +15,8 @@ import "../../styles/main.css"; // One static account page per locale: /account, /es/account, /eu/account. export function getStaticPaths() { return [ - { params: { lang: undefined }, props: { locale: "en" as Lang } }, - { params: { lang: "es" }, props: { locale: "es" as Lang } }, + { params: { lang: undefined }, props: { locale: "es" as Lang } }, + { params: { lang: "en" }, props: { locale: "en" as Lang } }, { params: { lang: "eu" }, props: { locale: "eu" as Lang } }, ]; } @@ -28,6 +28,7 @@ const { theme } = site; const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify; const homePath = LANG_PATH[locale]; const registerPath = LANG_REGISTER_PATH[locale]; +const accountPath = LANG_ACCOUNT_PATH[locale]; // Strings the client script needs at runtime (kept minimal, passed via define:vars). const clientCfg = { @@ -39,6 +40,7 @@ const clientCfg = { skinUpload: a.skinUpload, skinUploading: a.skinUploading, skinOk: a.skinOk, + refresh: a.refresh, skinPreviewNote: a.skinPreviewNote, resetSkin: a.resetSkin, resetConfirm: a.resetConfirm, @@ -61,7 +63,7 @@ const clientCfg = { {site.name} — {a.title} - + {LANGS.map((l) => )} @@ -86,6 +88,8 @@ const clientCfg = { >{LANG_LABEL[l]} ))}
+ {t.nav.register} + {t.nav.login} {t.nav.play}
@@ -98,25 +102,27 @@ const clientCfg = {

{a.title}

{a.lead}

- -
-
- - -
-
- - -
- - -
+ +
+
+
+ + +
+
+ + +
+ + +
-

- {a.noAccount} {a.registerLink} -

+

+ {a.noAccount} {a.registerLink} +

+