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 0000000..88fda32 Binary files /dev/null and b/landing/public/favicon.png differ diff --git a/landing/src/components/PixelIcon.astro b/landing/src/components/PixelIcon.astro index 37638a7..f62333d 100644 --- a/landing/src/components/PixelIcon.astro +++ b/landing/src/components/PixelIcon.astro @@ -1,10 +1,13 @@ --- -// 7x7 feature glyphs — styled by .picon in main.css. +// Feature icon. Default: a 7x7 procedural glyph (styled by .picon in main.css). +// Optional: pass `img` (an absolute path like "/icons/foo.png|svg" under +// landing/public/) to render a custom image instead of a glyph. interface Props { glyph: "shield" | "diamond" | "orb" | "heart"; gold?: boolean; + img?: string; // custom icon path in /public/icons/ — overrides the glyph } -const { glyph, gold = false } = Astro.props; +const { glyph, gold = false, img } = Astro.props; const GLYPHS: Record = { 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} +

+