feat(landing): rework — es default, 2-step join, roster + account polish
- 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/<name>.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 <noreply@anthropic.com>
This commit is contained in:
21
.env.example
21
.env.example
@@ -7,18 +7,29 @@ BASE_DOMAIN=ulicraft.net
|
|||||||
|
|
||||||
RCON_PASSWORD=change-me-to-something-random
|
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
|
# invite = closed; guests need an admin-minted invite code (recommended on a
|
||||||
# public, internet-present host). Admin mints via POST /drasl/api/v2/invites.
|
# public, internet-present host). Admin mints via POST /drasl/api/v2/invites.
|
||||||
# open = anyone can self-register with username+password (set [RateLimit]!).
|
# open = anyone can self-register with username+password (set [RateLimit]!).
|
||||||
# Consumed by BOTH render-config.sh (-> Drasl RegistrationNewPlayer.RequireInvite)
|
# Consumed by render-config.sh (-> Drasl RegistrationNewPlayer.RequireInvite).
|
||||||
# and the landing build (-> show/hide the invite-code field). Default: invite.
|
# Default: invite. NOTE: this no longer controls the landing invite FIELD — that's
|
||||||
|
# REGISTRATION_SHOW_INVITE below.
|
||||||
REGISTRATION_MODE=invite
|
REGISTRATION_MODE=invite
|
||||||
|
|
||||||
# NMSR avatar render mode for the landing's avatar tiles (online players +
|
# Show the invite-code FIELD on the landing register form. Read by the landing
|
||||||
# Members roster). Read by the landing BUILD (data/site.ts) only. Avatar URL =
|
# 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}/<mode>/<uuid>?size=N. e.g. headiso | head | fullbody.
|
# https://avatar.${BASE_DOMAIN}/<mode>/<uuid>?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
|
AVATAR_MODE=headiso
|
||||||
|
ROSTER_AVATAR_MODE=fullbodyiso
|
||||||
|
|
||||||
# Drasl admin creds for the registered-players roster (/api/mcstatus/players).
|
# Drasl admin creds for the registered-players roster (/api/mcstatus/players).
|
||||||
# Consumed ONLY by the mc-status container (server-side login → sanitized
|
# Consumed ONLY by the mc-status container (server-side login → sanitized
|
||||||
|
|||||||
BIN
landing/public/favicon.png
Normal file
BIN
landing/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
@@ -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 {
|
interface Props {
|
||||||
glyph: "shield" | "diamond" | "orb" | "heart";
|
glyph: "shield" | "diamond" | "orb" | "heart";
|
||||||
gold?: boolean;
|
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<Props["glyph"], string[]> = {
|
const GLYPHS: Record<Props["glyph"], string[]> = {
|
||||||
shield: ["0111110", "1111111", "1111111", "1111111", "0111110", "0011100", "0001000"],
|
shield: ["0111110", "1111111", "1111111", "1111111", "0111110", "0011100", "0001000"],
|
||||||
@@ -15,6 +18,10 @@ const GLYPHS: Record<Props["glyph"], string[]> = {
|
|||||||
const cells = GLYPHS[glyph].join("").split("");
|
const cells = GLYPHS[glyph].join("").split("");
|
||||||
const onClass = gold ? "go" : "on";
|
const onClass = gold ? "go" : "on";
|
||||||
---
|
---
|
||||||
<div class="picon" aria-hidden="true">
|
{img ? (
|
||||||
|
<img class="picon-img" src={img} alt="" aria-hidden="true" width="52" height="52" />
|
||||||
|
) : (
|
||||||
|
<div class="picon" aria-hidden="true">
|
||||||
{cells.map((c) => <i class={c === "1" ? onClass : undefined} />)}
|
{cells.map((c) => <i class={c === "1" ? onClass : undefined} />)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
const { emptyLabel = "No members yet." } = Astro.props;
|
const { emptyLabel = "No members yet." } = Astro.props;
|
||||||
|
|
||||||
const { status, avatarUrl, avatarMode } = site;
|
const { status, avatarUrl, rosterAvatarMode } = site;
|
||||||
---
|
---
|
||||||
<div
|
<div
|
||||||
class="roster-block"
|
class="roster-block"
|
||||||
data-api={status.rosterApi}
|
data-api={status.rosterApi}
|
||||||
data-avatar={avatarUrl}
|
data-avatar={avatarUrl}
|
||||||
data-mode={avatarMode}
|
data-mode={rosterAvatarMode}
|
||||||
data-empty={emptyLabel}
|
data-empty={emptyLabel}
|
||||||
>
|
>
|
||||||
<div class="roster" aria-label="Registered players"></div>
|
<div class="roster" aria-label="Registered players"></div>
|
||||||
@@ -58,7 +58,10 @@ const { status, avatarUrl, avatarMode } = site;
|
|||||||
return showEmpty();
|
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();
|
if (players.length === 0) return showEmpty();
|
||||||
|
|
||||||
grid.innerHTML = "";
|
grid.innerHTML = "";
|
||||||
@@ -75,7 +78,7 @@ const { status, avatarUrl, avatarMode } = site;
|
|||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.loading = "lazy";
|
img.loading = "lazy";
|
||||||
img.alt = name;
|
img.alt = name;
|
||||||
img.src = `${avatar}/${mode}/${uuid}?size=64`;
|
img.src = `${avatar}/${mode}/${uuid}?size=128`;
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
label.className = "roster-name";
|
label.className = "roster-name";
|
||||||
label.textContent = name;
|
label.textContent = name;
|
||||||
|
|||||||
@@ -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.
|
// is absent/empty (fresh checkout), fall back to the honest "50+" estimate.
|
||||||
const MOD_COUNT_KEY = MODS.count > 0 ? `${MODS.count}` : "50+";
|
const MOD_COUNT_KEY = MODS.count > 0 ? `${MODS.count}` : "50+";
|
||||||
|
|
||||||
// NMSR render mode for avatar tiles (online roster + members grid). Read at
|
// NMSR render mode for avatar tiles. AVATAR_MODE is the ServerCard online row +
|
||||||
// build time, same as BASE_DOMAIN. e.g. "headiso" | "head" | "fullbody".
|
// 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.
|
// @ts-ignore — `process` is untyped here (no @types/node), same as BASE_DOMAIN.
|
||||||
const AVATAR_MODE = process.env.AVATAR_MODE ?? "headiso";
|
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
|
// Launcher download manifest (per launcher-manifest.schema.json). The
|
||||||
// custom-launcher release flow syncs launcher-manifest.json into this dir
|
// 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.
|
// render-config.sh. "invite" => the register form must collect an invite code.
|
||||||
const REGISTRATION_MODE = process.env.REGISTRATION_MODE ?? "invite";
|
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.
|
// Minecraft / pack facts, surfaced in a few places.
|
||||||
const MC_VERSION = "1.21.1";
|
const MC_VERSION = "1.21.1";
|
||||||
|
|
||||||
@@ -97,9 +110,12 @@ export const site = {
|
|||||||
mcVersion: MC_VERSION,
|
mcVersion: MC_VERSION,
|
||||||
|
|
||||||
// Register form behaviour, driven by REGISTRATION_MODE in .env.
|
// 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
|
// false => open self-registration
|
||||||
registrationRequiresInvite: REGISTRATION_MODE === "invite",
|
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.
|
// Design knobs (replace the design's in-browser TweaksPanel). Baked at build.
|
||||||
// mood: "grass" | "nether" | "end"
|
// mood: "grass" | "nether" | "end"
|
||||||
@@ -127,9 +143,11 @@ export const site = {
|
|||||||
// Mojang/Crafatar — head URL is `${avatarUrl}/headiso/<uuid>?size=64`.
|
// Mojang/Crafatar — head URL is `${avatarUrl}/headiso/<uuid>?size=64`.
|
||||||
avatarUrl: `https://avatar.${BASE_DOMAIN}`,
|
avatarUrl: `https://avatar.${BASE_DOMAIN}`,
|
||||||
|
|
||||||
// NMSR render mode for avatar tiles (ServerCard online row + PlayerRoster
|
// NMSR render mode for avatar tiles. Avatar URL = `${avatarUrl}/<mode>/<uuid>?size=N`.
|
||||||
// members grid). Avatar URL = `${avatarUrl}/${avatarMode}/<uuid>?size=N`.
|
// avatarMode → ServerCard online row + account preview (heads)
|
||||||
|
// rosterAvatarMode → PlayerRoster members grid (full body)
|
||||||
avatarMode: AVATAR_MODE,
|
avatarMode: AVATAR_MODE,
|
||||||
|
rosterAvatarMode: ROSTER_AVATAR_MODE,
|
||||||
|
|
||||||
// Uptime Kuma status page (external link in footer).
|
// Uptime Kuma status page (external link in footer).
|
||||||
statusUrl: `https://status.${BASE_DOMAIN}`,
|
statusUrl: `https://status.${BASE_DOMAIN}`,
|
||||||
|
|||||||
@@ -11,31 +11,31 @@
|
|||||||
export type Lang = "en" | "es" | "eu";
|
export type Lang = "en" | "es" | "eu";
|
||||||
export const LANGS: Lang[] = ["en", "es", "eu"];
|
export const LANGS: Lang[] = ["en", "es", "eu"];
|
||||||
export const LANG_LABEL: Record<Lang, string> = { en: "EN", es: "ES", eu: "EU" };
|
export const LANG_LABEL: Record<Lang, string> = { en: "EN", es: "ES", eu: "EU" };
|
||||||
// URL path per locale (en is default, served at root).
|
// URL path per locale (es is default, served at root; en moves to /en/).
|
||||||
export const LANG_PATH: Record<Lang, string> = { en: "/", es: "/es/", eu: "/eu/" };
|
export const LANG_PATH: Record<Lang, string> = { es: "/", en: "/en/", eu: "/eu/" };
|
||||||
// Register page path per locale (mirrors LANG_PATH: en at /register).
|
// Register page path per locale (mirrors LANG_PATH: es at /register).
|
||||||
export const LANG_REGISTER_PATH: Record<Lang, string> = {
|
export const LANG_REGISTER_PATH: Record<Lang, string> = {
|
||||||
en: "/register",
|
es: "/register",
|
||||||
es: "/es/register",
|
en: "/en/register",
|
||||||
eu: "/eu/register",
|
eu: "/eu/register",
|
||||||
};
|
};
|
||||||
// Fjord (alternative path) page path per locale (mirrors LANG_REGISTER_PATH).
|
// Fjord (alternative path) page path per locale (mirrors LANG_REGISTER_PATH).
|
||||||
export const LANG_FJORD_PATH: Record<Lang, string> = {
|
export const LANG_FJORD_PATH: Record<Lang, string> = {
|
||||||
en: "/fjord",
|
es: "/fjord",
|
||||||
es: "/es/fjord",
|
en: "/en/fjord",
|
||||||
eu: "/eu/fjord",
|
eu: "/eu/fjord",
|
||||||
};
|
};
|
||||||
// Account page path per locale (mirrors LANG_REGISTER_PATH).
|
// Account page path per locale (mirrors LANG_REGISTER_PATH).
|
||||||
export const LANG_ACCOUNT_PATH: Record<Lang, string> = {
|
export const LANG_ACCOUNT_PATH: Record<Lang, string> = {
|
||||||
en: "/account",
|
es: "/account",
|
||||||
es: "/es/account",
|
en: "/en/account",
|
||||||
eu: "/eu/account",
|
eu: "/eu/account",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UI {
|
export interface UI {
|
||||||
htmlLang: string;
|
htmlLang: string;
|
||||||
copied: string; // transient copy-button feedback
|
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: {
|
hero: {
|
||||||
eyebrow: string; // "Modded LAN · NeoForge" — version appended at render
|
eyebrow: string; // "Modded LAN · NeoForge" — version appended at render
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -84,13 +84,6 @@ export interface UI {
|
|||||||
registerLink: string;
|
registerLink: string;
|
||||||
};
|
};
|
||||||
s2: { kicker: string; h: string; p: 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: {
|
fjord: {
|
||||||
navHome: string; // "← Home" link in nav
|
navHome: string; // "← Home" link in nav
|
||||||
@@ -164,6 +157,7 @@ export interface UI {
|
|||||||
noAccount: string; // "No account yet?"
|
noAccount: string; // "No account yet?"
|
||||||
registerLink: string; // links to /register
|
registerLink: string; // links to /register
|
||||||
// Skin panel
|
// Skin panel
|
||||||
|
refresh: string; // re-fetch the NMSR avatar
|
||||||
playerLabel: string; // "Logged in as"
|
playerLabel: string; // "Logged in as"
|
||||||
skinTitle: string;
|
skinTitle: string;
|
||||||
skinLead: string; // reuses register.skinLead concept
|
skinLead: string; // reuses register.skinLead concept
|
||||||
@@ -189,7 +183,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
en: {
|
en: {
|
||||||
htmlLang: "en",
|
htmlLang: "en",
|
||||||
copied: "Copied!",
|
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: {
|
hero: {
|
||||||
eyebrow: "Modded LAN · NeoForge",
|
eyebrow: "Modded LAN · NeoForge",
|
||||||
tagline: "Kitchen-sink survival, built to outlast the weekend.",
|
tagline: "Kitchen-sink survival, built to outlast the weekend.",
|
||||||
@@ -249,7 +243,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
empty: "Mod list not generated yet — check back soon.",
|
empty: "Mod list not generated yet — check back soon.",
|
||||||
},
|
},
|
||||||
join: {
|
join: {
|
||||||
eyebrow: "How to Join · 3 Steps",
|
eyebrow: "How to Join · 2 Steps",
|
||||||
title: "From zero to spawning in.",
|
title: "From zero to spawning in.",
|
||||||
lead: "One-time setup. After this, the launcher keeps you in sync.",
|
lead: "One-time setup. After this, the launcher keeps you in sync.",
|
||||||
s1: {
|
s1: {
|
||||||
@@ -262,14 +256,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
s2: {
|
s2: {
|
||||||
kicker: "Launcher",
|
kicker: "Launcher",
|
||||||
h: "Download {name}",
|
h: "Download {name}",
|
||||||
p: "Grab {name} for your system and log in with your Ulicraft credentials — it installs the modpack and everything else automatically.",
|
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.",
|
||||||
},
|
|
||||||
s3: {
|
|
||||||
kicker: "Connect",
|
|
||||||
h: "Join the server",
|
|
||||||
pa: "Launch the launcher, open",
|
|
||||||
pb: ", paste the address, and join.",
|
|
||||||
copy: "Copy IP",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fjord: {
|
fjord: {
|
||||||
@@ -354,6 +341,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
submitting: "Logging in…",
|
submitting: "Logging in…",
|
||||||
noAccount: "No account yet?",
|
noAccount: "No account yet?",
|
||||||
registerLink: "Register here",
|
registerLink: "Register here",
|
||||||
|
refresh: "Refresh",
|
||||||
playerLabel: "Logged in as",
|
playerLabel: "Logged in as",
|
||||||
skinTitle: "Your skin",
|
skinTitle: "Your skin",
|
||||||
skinLead: "Upload a Minecraft skin PNG. The 3D preview below updates instantly; the in-game avatar takes ~1 minute.",
|
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<Lang, UI> = {
|
|||||||
es: {
|
es: {
|
||||||
htmlLang: "es",
|
htmlLang: "es",
|
||||||
copied: "¡Copiado!",
|
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: {
|
hero: {
|
||||||
eyebrow: "LAN con mods · NeoForge",
|
eyebrow: "LAN con mods · NeoForge",
|
||||||
tagline: "Supervivencia todo incluido, para durar más que el finde.",
|
tagline: "Supervivencia todo incluido, para durar más que el finde.",
|
||||||
@@ -437,7 +425,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
empty: "La lista de mods aún no está generada — vuelve pronto.",
|
empty: "La lista de mods aún no está generada — vuelve pronto.",
|
||||||
},
|
},
|
||||||
join: {
|
join: {
|
||||||
eyebrow: "Cómo entrar · 3 pasos",
|
eyebrow: "Cómo entrar · 2 pasos",
|
||||||
title: "De cero a aparecer en el mundo.",
|
title: "De cero a aparecer en el mundo.",
|
||||||
lead: "Configuración única. Después, el launcher te mantiene sincronizado.",
|
lead: "Configuración única. Después, el launcher te mantiene sincronizado.",
|
||||||
s1: {
|
s1: {
|
||||||
@@ -450,14 +438,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
s2: {
|
s2: {
|
||||||
kicker: "Launcher",
|
kicker: "Launcher",
|
||||||
h: "Descarga {name}",
|
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.",
|
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.",
|
||||||
},
|
|
||||||
s3: {
|
|
||||||
kicker: "Conectar",
|
|
||||||
h: "Entra al servidor",
|
|
||||||
pa: "Lanza el launcher, abre",
|
|
||||||
pb: ", pega la dirección y entra.",
|
|
||||||
copy: "Copiar IP",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fjord: {
|
fjord: {
|
||||||
@@ -542,6 +523,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
submitting: "Entrando…",
|
submitting: "Entrando…",
|
||||||
noAccount: "¿Aún no tienes cuenta?",
|
noAccount: "¿Aún no tienes cuenta?",
|
||||||
registerLink: "Regístrate aquí",
|
registerLink: "Regístrate aquí",
|
||||||
|
refresh: "Actualizar",
|
||||||
playerLabel: "Sesión iniciada como",
|
playerLabel: "Sesión iniciada como",
|
||||||
skinTitle: "Tu skin",
|
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.",
|
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<Lang, UI> = {
|
|||||||
eu: {
|
eu: {
|
||||||
htmlLang: "eu",
|
htmlLang: "eu",
|
||||||
copied: "Kopiatuta!",
|
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: {
|
hero: {
|
||||||
eyebrow: "Mod-dun LANa · NeoForge",
|
eyebrow: "Mod-dun LANa · NeoForge",
|
||||||
tagline: "Mod ugariko biziraupena, irauteko egina.",
|
tagline: "Mod ugariko biziraupena, irauteko egina.",
|
||||||
@@ -625,7 +607,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
empty: "Mod zerrenda oraindik ez da sortu — itzuli laster.",
|
empty: "Mod zerrenda oraindik ez da sortu — itzuli laster.",
|
||||||
},
|
},
|
||||||
join: {
|
join: {
|
||||||
eyebrow: "Nola sartu · 3 urrats",
|
eyebrow: "Nola sartu · 2 urrats",
|
||||||
title: "Zerotik mundura agertzera.",
|
title: "Zerotik mundura agertzera.",
|
||||||
lead: "Behin bakarrik konfiguratu. Gero, launcherrak sinkronizatuta mantenduko zaitu.",
|
lead: "Behin bakarrik konfiguratu. Gero, launcherrak sinkronizatuta mantenduko zaitu.",
|
||||||
s1: {
|
s1: {
|
||||||
@@ -638,14 +620,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
s2: {
|
s2: {
|
||||||
kicker: "Launcherra",
|
kicker: "Launcherra",
|
||||||
h: "Deskargatu {name}",
|
h: "Deskargatu {name}",
|
||||||
p: "Hartu {name} zure sistemarako eta hasi saioa zure Ulicraft kredentzialekin: modpacka eta gainerako guztia automatikoki instalatzen du.",
|
p: "Hartu {name}, hasi saioa zure Ulicraft kredentzialekin eta sakatu jolastu. Gainerako guztia — modpacka, zerbitzariaren helbidea, eguneraketak — launcherraren barruan kudeatzen da.",
|
||||||
},
|
|
||||||
s3: {
|
|
||||||
kicker: "Konektatu",
|
|
||||||
h: "Sartu zerbitzarira",
|
|
||||||
pa: "Abiarazi launcherra, ireki",
|
|
||||||
pb: ", itsatsi helbidea eta sartu.",
|
|
||||||
copy: "Kopiatu IPa",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fjord: {
|
fjord: {
|
||||||
@@ -730,6 +705,7 @@ export const ui: Record<Lang, UI> = {
|
|||||||
submitting: "Sartzen…",
|
submitting: "Sartzen…",
|
||||||
noAccount: "Oraindik konturik ez?",
|
noAccount: "Oraindik konturik ez?",
|
||||||
registerLink: "Erregistratu hemen",
|
registerLink: "Erregistratu hemen",
|
||||||
|
refresh: "Eguneratu",
|
||||||
playerLabel: "Saioa hasita:",
|
playerLabel: "Saioa hasita:",
|
||||||
skinTitle: "Zure azala",
|
skinTitle: "Zure azala",
|
||||||
skinLead: "Igo Minecraft azal PNG bat. 3D aurrebista berehala eguneratzen da; jokoko avatarra ~1 minutuan.",
|
skinLead: "Igo Minecraft azal PNG bat. 3D aurrebista berehala eguneratzen da; jokoko avatarra ~1 minutuan.",
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
import { site, LITERALS, HEAD_FONTS } from "../data/site";
|
import { site, HEAD_FONTS } from "../data/site";
|
||||||
import { ui, LANGS, LANG_LABEL, LANG_PATH, LANG_REGISTER_PATH, type Lang } from "../i18n/ui";
|
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 Creeper from "../components/Creeper.astro";
|
||||||
import PixelIcon from "../components/PixelIcon.astro";
|
import PixelIcon from "../components/PixelIcon.astro";
|
||||||
import CopyChip from "../components/CopyChip.astro";
|
|
||||||
import ServerCard from "../components/ServerCard.astro";
|
import ServerCard from "../components/ServerCard.astro";
|
||||||
import PlayerRoster from "../components/PlayerRoster.astro";
|
import PlayerRoster from "../components/PlayerRoster.astro";
|
||||||
import ModList from "../components/ModList.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/".
|
// One static page per locale: en at "/", es at "/es/", eu at "/eu/".
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return [
|
return [
|
||||||
{ params: { lang: undefined }, props: { locale: "en" as Lang } },
|
{ params: { lang: undefined }, props: { locale: "es" as Lang } },
|
||||||
{ params: { lang: "es" }, props: { locale: "es" as Lang } },
|
{ params: { lang: "en" }, props: { locale: "en" as Lang } },
|
||||||
{ params: { lang: "eu" }, props: { locale: "eu" 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 headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
||||||
const launcherName = launcherManifest.product;
|
const launcherName = launcherManifest.product;
|
||||||
const registerPath = LANG_REGISTER_PATH[locale];
|
const registerPath = LANG_REGISTER_PATH[locale];
|
||||||
|
const accountPath = LANG_ACCOUNT_PATH[locale];
|
||||||
|
|
||||||
// Per-OS download buttons (join step 2) from the synced manifest. Missing
|
// Per-OS download buttons (join step 2) from the synced manifest. Missing
|
||||||
// manifest → empty builds → no buttons (the build never fails on it).
|
// manifest → empty builds → no buttons (the build never fails on it).
|
||||||
@@ -60,7 +60,7 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{site.name} — {t.hero.howToJoin}</title>
|
<title>{site.name} — {t.hero.howToJoin}</title>
|
||||||
<meta name="description" content={`${site.name}: ${t.hero.tagline}`} />
|
<meta name="description" content={`${site.name}: ${t.hero.tagline}`} />
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_PATH[l]} />)}
|
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_PATH[l]} />)}
|
||||||
</head>
|
</head>
|
||||||
@@ -73,11 +73,11 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
<span class="name">ULICRAFT</span>
|
<span class="name">ULICRAFT</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="nav-links">
|
<nav class="nav-links">
|
||||||
|
<a href="#join">{t.nav.join}</a>
|
||||||
<a href="#status">{t.nav.status}</a>
|
<a href="#status">{t.nav.status}</a>
|
||||||
<a href="#members">{t.nav.members}</a>
|
<a href="#members">{t.nav.members}</a>
|
||||||
<a href="#features">{t.nav.features}</a>
|
|
||||||
<a href="#mods">{t.nav.mods}</a>
|
<a href="#mods">{t.nav.mods}</a>
|
||||||
<a href="#join">{t.nav.join}</a>
|
<a href="#features">{t.nav.features}</a>
|
||||||
</nav>
|
</nav>
|
||||||
<span class="nav-spacer"></span>
|
<span class="nav-spacer"></span>
|
||||||
<div class="lang-switch" aria-label="Language">
|
<div class="lang-switch" aria-label="Language">
|
||||||
@@ -85,6 +85,8 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
<a href={LANG_PATH[l]} class:list={[l === locale && "on"]} aria-current={l === locale ? "true" : undefined}>{LANG_LABEL[l]}</a>
|
<a href={LANG_PATH[l]} class:list={[l === locale && "on"]} aria-current={l === locale ? "true" : undefined}>{LANG_LABEL[l]}</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<a class="mc-btn sm" href={registerPath}>{t.nav.register}</a>
|
||||||
|
<a class="mc-btn sm" href={accountPath}>{t.nav.login}</a>
|
||||||
<a class="mc-btn primary sm" href="#join">{t.nav.play}</a>
|
<a class="mc-btn primary sm" href="#join">{t.nav.play}</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -110,8 +112,7 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
<h1 class="hero-tagline">{t.hero.tagline}</h1>
|
<h1 class="hero-tagline">{t.hero.tagline}</h1>
|
||||||
<p class="hero-sub">{t.hero.sub}</p>
|
<p class="hero-sub">{t.hero.sub}</p>
|
||||||
<div class="hero-ip-row">
|
<div class="hero-ip-row">
|
||||||
<CopyChip value={site.serverAddress} variant="ip" label={t.hero.copyIp} copiedLabel={t.copied} />
|
<a class="mc-btn primary" href="#join">{t.hero.howToJoin}</a>
|
||||||
<a class="mc-btn" href="#join">{t.hero.howToJoin}</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span>Java {site.mcVersion}</span>
|
<span>Java {site.mcVersion}</span>
|
||||||
@@ -131,82 +132,6 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- STATUS -->
|
|
||||||
<section id="status" class="pad">
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="sec-head reveal">
|
|
||||||
<span class="eyebrow">{t.status.eyebrow}</span>
|
|
||||||
<h2 class="section-title">{t.status.title}</h2>
|
|
||||||
<p class="lead">{t.status.lead}</p>
|
|
||||||
</div>
|
|
||||||
<div class="reveal">
|
|
||||||
<ServerCard
|
|
||||||
modded={t.status.modded}
|
|
||||||
motd={t.status.motd}
|
|
||||||
upTo={t.status.upTo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="stat-grid">
|
|
||||||
{site.stats.map((s, i) => (
|
|
||||||
<div class="tile reveal" style={`transition-delay:${i * 60}ms`}>
|
|
||||||
<div class="k">{s.key}</div>
|
|
||||||
<div class="l">{t.statLabels[i]}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- MEMBERS (registered players roster) -->
|
|
||||||
<section id="members" class="pad" style="background: var(--bg-2)">
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="sec-head reveal">
|
|
||||||
<span class="eyebrow">{t.members.eyebrow}</span>
|
|
||||||
<h2 class="section-title">{t.members.title}</h2>
|
|
||||||
<p class="lead">{t.members.lead}</p>
|
|
||||||
</div>
|
|
||||||
<div class="reveal">
|
|
||||||
<PlayerRoster emptyLabel={t.members.empty} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- FEATURES -->
|
|
||||||
<section id="features" class="pad">
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="sec-head reveal">
|
|
||||||
<span class="eyebrow">{t.features.eyebrow}</span>
|
|
||||||
<h2 class="section-title">{t.features.title}</h2>
|
|
||||||
<p class="lead">{t.features.lead}</p>
|
|
||||||
</div>
|
|
||||||
<div class="feat-grid">
|
|
||||||
{site.features.map((f, i) => (
|
|
||||||
<div class="feat reveal" style={`transition-delay:${(i % 2) * 80}ms`}>
|
|
||||||
<PixelIcon glyph={f.glyph} gold={f.gold} />
|
|
||||||
<div>
|
|
||||||
<h3>{t.features.items[i].h}</h3>
|
|
||||||
<p>{t.features.items[i].p}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- MODS (installed-pack list, auto-generated) -->
|
|
||||||
<section id="mods" class="pad" style="background: var(--bg-2)">
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="sec-head reveal">
|
|
||||||
<span class="eyebrow">{t.mods.eyebrow}</span>
|
|
||||||
<h2 class="section-title">{t.mods.title}</h2>
|
|
||||||
<p class="lead">{t.mods.lead}</p>
|
|
||||||
</div>
|
|
||||||
<div class="reveal">
|
|
||||||
<ModList emptyLabel={t.mods.empty} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- HOW TO JOIN -->
|
<!-- HOW TO JOIN -->
|
||||||
<section id="join" class="pad">
|
<section id="join" class="pad">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
@@ -247,21 +172,82 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- STEP 3 -->
|
<!-- STATUS -->
|
||||||
<div class="step reveal">
|
<section id="status" class="pad">
|
||||||
<div class="num">3</div>
|
<div class="wrap">
|
||||||
<div class="body">
|
<div class="sec-head reveal">
|
||||||
<span class="kicker">{t.join.s3.kicker}</span>
|
<span class="eyebrow">{t.status.eyebrow}</span>
|
||||||
<h3>{t.join.s3.h}</h3>
|
<h2 class="section-title">{t.status.title}</h2>
|
||||||
<p>
|
<p class="lead">{t.status.lead}</p>
|
||||||
{t.join.s3.pa} <kbd>{LITERALS.multiplayer}</kbd> → <kbd>{LITERALS.addServer}</kbd>{t.join.s3.pb}
|
</div>
|
||||||
</p>
|
<div class="reveal">
|
||||||
<div class="actions">
|
<ServerCard
|
||||||
<CopyChip value={site.serverAddress} variant="ip" label={t.join.s3.copy} copiedLabel={t.copied} />
|
modded={t.status.modded}
|
||||||
|
motd={t.status.motd}
|
||||||
|
upTo={t.status.upTo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="stat-grid">
|
||||||
|
{site.stats.map((s, i) => (
|
||||||
|
<div class="tile reveal" style={`transition-delay:${i * 60}ms`}>
|
||||||
|
<div class="k">{s.key}</div>
|
||||||
|
<div class="l">{t.statLabels[i]}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MEMBERS (registered players roster) -->
|
||||||
|
<section id="members" class="pad" style="background: var(--bg-2)">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<span class="eyebrow">{t.members.eyebrow}</span>
|
||||||
|
<h2 class="section-title">{t.members.title}</h2>
|
||||||
|
<p class="lead">{t.members.lead}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="reveal">
|
||||||
|
<PlayerRoster emptyLabel={t.members.empty} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MODS (installed-pack list, auto-generated) -->
|
||||||
|
<section id="mods" class="pad" style="background: var(--bg-2)">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<span class="eyebrow">{t.mods.eyebrow}</span>
|
||||||
|
<h2 class="section-title">{t.mods.title}</h2>
|
||||||
|
<p class="lead">{t.mods.lead}</p>
|
||||||
|
</div>
|
||||||
|
<div class="reveal">
|
||||||
|
<ModList emptyLabel={t.mods.empty} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FEATURES -->
|
||||||
|
<section id="features" class="pad">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<span class="eyebrow">{t.features.eyebrow}</span>
|
||||||
|
<h2 class="section-title">{t.features.title}</h2>
|
||||||
|
<p class="lead">{t.features.lead}</p>
|
||||||
|
</div>
|
||||||
|
<div class="feat-grid">
|
||||||
|
{site.features.map((f, i) => (
|
||||||
|
<div class="feat reveal" style={`transition-delay:${(i % 2) * 80}ms`}>
|
||||||
|
<PixelIcon glyph={f.glyph} gold={f.gold} />
|
||||||
|
<div>
|
||||||
|
<h3>{t.features.items[i].h}</h3>
|
||||||
|
<p>{t.features.items[i].p}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import "../../styles/main.css";
|
|||||||
// One static account page per locale: /account, /es/account, /eu/account.
|
// One static account page per locale: /account, /es/account, /eu/account.
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return [
|
return [
|
||||||
{ params: { lang: undefined }, props: { locale: "en" as Lang } },
|
{ params: { lang: undefined }, props: { locale: "es" as Lang } },
|
||||||
{ params: { lang: "es" }, props: { locale: "es" as Lang } },
|
{ params: { lang: "en" }, props: { locale: "en" as Lang } },
|
||||||
{ params: { lang: "eu" }, props: { locale: "eu" 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 headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
||||||
const homePath = LANG_PATH[locale];
|
const homePath = LANG_PATH[locale];
|
||||||
const registerPath = LANG_REGISTER_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).
|
// Strings the client script needs at runtime (kept minimal, passed via define:vars).
|
||||||
const clientCfg = {
|
const clientCfg = {
|
||||||
@@ -39,6 +40,7 @@ const clientCfg = {
|
|||||||
skinUpload: a.skinUpload,
|
skinUpload: a.skinUpload,
|
||||||
skinUploading: a.skinUploading,
|
skinUploading: a.skinUploading,
|
||||||
skinOk: a.skinOk,
|
skinOk: a.skinOk,
|
||||||
|
refresh: a.refresh,
|
||||||
skinPreviewNote: a.skinPreviewNote,
|
skinPreviewNote: a.skinPreviewNote,
|
||||||
resetSkin: a.resetSkin,
|
resetSkin: a.resetSkin,
|
||||||
resetConfirm: a.resetConfirm,
|
resetConfirm: a.resetConfirm,
|
||||||
@@ -61,7 +63,7 @@ const clientCfg = {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{site.name} — {a.title}</title>
|
<title>{site.name} — {a.title}</title>
|
||||||
<meta name="description" content={a.lead} />
|
<meta name="description" content={a.lead} />
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_ACCOUNT_PATH[l]} />)}
|
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_ACCOUNT_PATH[l]} />)}
|
||||||
</head>
|
</head>
|
||||||
@@ -86,6 +88,8 @@ const clientCfg = {
|
|||||||
>{LANG_LABEL[l]}</a>
|
>{LANG_LABEL[l]}</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<a class="mc-btn sm" href={registerPath}>{t.nav.register}</a>
|
||||||
|
<a class="mc-btn sm" href={accountPath}>{t.nav.login}</a>
|
||||||
<a class="mc-btn primary sm" href={`${homePath}#join`}>{t.nav.play}</a>
|
<a class="mc-btn primary sm" href={`${homePath}#join`}>{t.nav.play}</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -98,7 +102,8 @@ const clientCfg = {
|
|||||||
<h1>{a.title}</h1>
|
<h1>{a.title}</h1>
|
||||||
<p class="lead">{a.lead}</p>
|
<p class="lead">{a.lead}</p>
|
||||||
|
|
||||||
<!-- LOGIN FORM -->
|
<!-- LOGIN (hidden once authenticated) -->
|
||||||
|
<div id="acc-login">
|
||||||
<form class="reg-form" id="acc-form" novalidate>
|
<form class="reg-form" id="acc-form" novalidate>
|
||||||
<div class="reg-field">
|
<div class="reg-field">
|
||||||
<label for="acc-username">{a.usernameLabel}</label>
|
<label for="acc-username">{a.usernameLabel}</label>
|
||||||
@@ -117,6 +122,7 @@ const clientCfg = {
|
|||||||
<p class="reg-foot">
|
<p class="reg-foot">
|
||||||
{a.noAccount} <a href={registerPath}>{a.registerLink}</a>
|
{a.noAccount} <a href={registerPath}>{a.registerLink}</a>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SKIN PANEL (revealed after login) -->
|
<!-- SKIN PANEL (revealed after login) -->
|
||||||
<div class="reg-success" id="acc-panel" hidden>
|
<div class="reg-success" id="acc-panel" hidden>
|
||||||
@@ -128,8 +134,9 @@ const clientCfg = {
|
|||||||
<!-- Current avatar preview (NMSR) -->
|
<!-- Current avatar preview (NMSR) -->
|
||||||
<div id="acc-avatar-wrap" style="display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap">
|
<div id="acc-avatar-wrap" style="display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap">
|
||||||
<img id="acc-avatar" src="" alt="avatar" width="96" height="96"
|
<img id="acc-avatar" src="" alt="avatar" width="96" height="96"
|
||||||
style="image-rendering:pixelated;width:96px;height:96px;background:var(--slot);box-shadow:inset 2px 2px 0 var(--bevel-lo),inset -2px -2px 0 var(--bevel-hi)" />
|
style="image-rendering:pixelated;width:96px;height:96px;background:var(--slot)" />
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:10px;align-items:flex-start">
|
||||||
|
<button class="mc-btn sm" id="acc-avatar-refresh" type="button">{a.refresh}</button>
|
||||||
<p class="reg-skin" style="border:none;padding-top:0;margin:0;color:var(--dim);font-size:13px" id="acc-preview-note"></p>
|
<p class="reg-skin" style="border:none;padding-top:0;margin:0;color:var(--dim);font-size:13px" id="acc-preview-note"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +191,7 @@ const clientCfg = {
|
|||||||
<script define:vars={{ cfg: clientCfg }}>
|
<script define:vars={{ cfg: clientCfg }}>
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const form = $("acc-form");
|
const form = $("acc-form");
|
||||||
|
const loginBox = $("acc-login");
|
||||||
const errBox = $("acc-error");
|
const errBox = $("acc-error");
|
||||||
const submitBtn = $("acc-submit");
|
const submitBtn = $("acc-submit");
|
||||||
const panel = $("acc-panel");
|
const panel = $("acc-panel");
|
||||||
@@ -234,8 +242,8 @@ const clientCfg = {
|
|||||||
apiToken = data.apiToken;
|
apiToken = data.apiToken;
|
||||||
player = data.user.players[0];
|
player = data.user.players[0];
|
||||||
|
|
||||||
// Show panel
|
// Show panel — hide the whole login block (form + heading foot).
|
||||||
form.hidden = true;
|
loginBox.hidden = true;
|
||||||
$("acc-player-name").textContent = player.name;
|
$("acc-player-name").textContent = player.name;
|
||||||
// Set NMSR avatar
|
// Set NMSR avatar
|
||||||
const avatarImg = $("acc-avatar");
|
const avatarImg = $("acc-avatar");
|
||||||
@@ -253,6 +261,12 @@ const clientCfg = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- Avatar refresh (re-fetch NMSR with a cache-bust) ----
|
||||||
|
$("acc-avatar-refresh").addEventListener("click", () => {
|
||||||
|
if (!player) return;
|
||||||
|
$("acc-avatar").src = `${cfg.avatarUrl}/${cfg.avatarMode}/${player.uuid}?size=96&t=${Date.now()}`;
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Skin upload ----
|
// ---- Skin upload ----
|
||||||
const fileInput = $("acc-skin-file");
|
const fileInput = $("acc-skin-file");
|
||||||
const fileName = $("acc-skin-name");
|
const fileName = $("acc-skin-name");
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
LANG_LABEL,
|
LANG_LABEL,
|
||||||
LANG_PATH,
|
LANG_PATH,
|
||||||
LANG_FJORD_PATH,
|
LANG_FJORD_PATH,
|
||||||
|
LANG_REGISTER_PATH,
|
||||||
|
LANG_ACCOUNT_PATH,
|
||||||
type Lang,
|
type Lang,
|
||||||
} from "../../i18n/ui";
|
} from "../../i18n/ui";
|
||||||
import Creeper from "../../components/Creeper.astro";
|
import Creeper from "../../components/Creeper.astro";
|
||||||
@@ -16,8 +18,8 @@ import "../../styles/main.css";
|
|||||||
// alternative manual path — intentionally NOT linked from the homepage nav.
|
// alternative manual path — intentionally NOT linked from the homepage nav.
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return [
|
return [
|
||||||
{ params: { lang: undefined }, props: { locale: "en" as Lang } },
|
{ params: { lang: undefined }, props: { locale: "es" as Lang } },
|
||||||
{ params: { lang: "es" }, props: { locale: "es" as Lang } },
|
{ params: { lang: "en" }, props: { locale: "en" as Lang } },
|
||||||
{ params: { lang: "eu" }, props: { locale: "eu" as Lang } },
|
{ params: { lang: "eu" }, props: { locale: "eu" as Lang } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -28,6 +30,8 @@ const f = t.fjord;
|
|||||||
const { theme, fjord } = site;
|
const { theme, fjord } = site;
|
||||||
const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
||||||
const homePath = LANG_PATH[locale];
|
const homePath = LANG_PATH[locale];
|
||||||
|
const registerPath = LANG_REGISTER_PATH[locale];
|
||||||
|
const accountPath = LANG_ACCOUNT_PATH[locale];
|
||||||
const fjordPack = site.launcherManifest.fjordPack;
|
const fjordPack = site.launcherManifest.fjordPack;
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,7 +47,7 @@ const fjordPack = site.launcherManifest.fjordPack;
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{site.name} — {f.title}</title>
|
<title>{site.name} — {f.title}</title>
|
||||||
<meta name="description" content={f.lead} />
|
<meta name="description" content={f.lead} />
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_FJORD_PATH[l]} />)}
|
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_FJORD_PATH[l]} />)}
|
||||||
</head>
|
</head>
|
||||||
@@ -68,6 +72,8 @@ const fjordPack = site.launcherManifest.fjordPack;
|
|||||||
>{LANG_LABEL[l]}</a>
|
>{LANG_LABEL[l]}</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<a class="mc-btn sm" href={registerPath}>{t.nav.register}</a>
|
||||||
|
<a class="mc-btn sm" href={accountPath}>{t.nav.login}</a>
|
||||||
<a class="mc-btn primary sm" href={`${homePath}#join`}>{t.nav.play}</a>
|
<a class="mc-btn primary sm" href={`${homePath}#join`}>{t.nav.play}</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import "../../styles/main.css";
|
|||||||
// One static register page per locale: /register, /es/register, /eu/register.
|
// One static register page per locale: /register, /es/register, /eu/register.
|
||||||
export function getStaticPaths() {
|
export function getStaticPaths() {
|
||||||
return [
|
return [
|
||||||
{ params: { lang: undefined }, props: { locale: "en" as Lang } },
|
{ params: { lang: undefined }, props: { locale: "es" as Lang } },
|
||||||
{ params: { lang: "es" }, props: { locale: "es" as Lang } },
|
{ params: { lang: "en" }, props: { locale: "en" as Lang } },
|
||||||
{ params: { lang: "eu" }, props: { locale: "eu" as Lang } },
|
{ params: { lang: "eu" }, props: { locale: "eu" as Lang } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -27,12 +27,16 @@ const r = t.register;
|
|||||||
const { theme } = site;
|
const { theme } = site;
|
||||||
const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
||||||
const homePath = LANG_PATH[locale];
|
const homePath = LANG_PATH[locale];
|
||||||
const requiresInvite = site.registrationRequiresInvite;
|
const registerPath = LANG_REGISTER_PATH[locale];
|
||||||
|
const accountPath = LANG_ACCOUNT_PATH[locale];
|
||||||
|
// Visibility of the invite-code field (default hidden). Independent of the
|
||||||
|
// backend gate (site.registrationRequiresInvite / Drasl RequireInvite).
|
||||||
|
const showInvite = site.registrationShowInvite;
|
||||||
|
|
||||||
// Strings the client script needs at runtime (kept minimal, passed via define:vars).
|
// Strings the client script needs at runtime (kept minimal, passed via define:vars).
|
||||||
const clientCfg = {
|
const clientCfg = {
|
||||||
apiUrl: site.draslApiUrl,
|
apiUrl: site.draslApiUrl,
|
||||||
requiresInvite,
|
showInvite,
|
||||||
submit: r.submit,
|
submit: r.submit,
|
||||||
submitting: r.submitting,
|
submitting: r.submitting,
|
||||||
skinUpload: r.skinUpload,
|
skinUpload: r.skinUpload,
|
||||||
@@ -57,7 +61,7 @@ const clientCfg = {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{site.name} — {r.title}</title>
|
<title>{site.name} — {r.title}</title>
|
||||||
<meta name="description" content={r.lead} />
|
<meta name="description" content={r.lead} />
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_REGISTER_PATH[l]} />)}
|
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_REGISTER_PATH[l]} />)}
|
||||||
</head>
|
</head>
|
||||||
@@ -82,6 +86,8 @@ const clientCfg = {
|
|||||||
>{LANG_LABEL[l]}</a>
|
>{LANG_LABEL[l]}</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<a class="mc-btn sm" href={registerPath}>{t.nav.register}</a>
|
||||||
|
<a class="mc-btn sm" href={accountPath}>{t.nav.login}</a>
|
||||||
<a class="mc-btn primary sm" href={`${homePath}#join`}>{t.nav.play}</a>
|
<a class="mc-btn primary sm" href={`${homePath}#join`}>{t.nav.play}</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -106,7 +112,7 @@ const clientCfg = {
|
|||||||
<input class="reg-input" id="reg-password" name="password"
|
<input class="reg-input" id="reg-password" name="password"
|
||||||
type="password" autocomplete="new-password" placeholder={r.passwordPlaceholder} required />
|
type="password" autocomplete="new-password" placeholder={r.passwordPlaceholder} required />
|
||||||
</div>
|
</div>
|
||||||
{requiresInvite && (
|
{showInvite && (
|
||||||
<div class="reg-field">
|
<div class="reg-field">
|
||||||
<label for="reg-invite">{r.inviteLabel}</label>
|
<label for="reg-invite">{r.inviteLabel}</label>
|
||||||
<input class="reg-input" id="reg-invite" name="invite"
|
<input class="reg-input" id="reg-invite" name="invite"
|
||||||
@@ -208,7 +214,7 @@ const clientCfg = {
|
|||||||
const password = $("reg-password").value;
|
const password = $("reg-password").value;
|
||||||
const inviteEl = $("reg-invite");
|
const inviteEl = $("reg-invite");
|
||||||
const invite = inviteEl ? inviteEl.value.trim() : null;
|
const invite = inviteEl ? inviteEl.value.trim() : null;
|
||||||
if (!username || !password || (cfg.requiresInvite && !invite)) {
|
if (!username || !password || (cfg.showInvite && !invite)) {
|
||||||
showErr(errBox, cfg.errFields);
|
showErr(errBox, cfg.errFields);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,11 +484,11 @@ section { position: relative; z-index: 1; }
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 8px;
|
padding: 12px 8px;
|
||||||
background: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: inset 1px 1px 0 var(--bevel-lo), inset -1px -1px 0 var(--bevel-hi);
|
|
||||||
}
|
}
|
||||||
.roster-tile img {
|
.roster-tile img {
|
||||||
width: 48px;
|
width: auto;
|
||||||
height: 48px;
|
height: 72px;
|
||||||
|
object-fit: contain;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
.roster-name {
|
.roster-name {
|
||||||
@@ -522,7 +522,7 @@ section { position: relative; z-index: 1; }
|
|||||||
.feat h3 { font-size: 21px; margin-bottom: 8px; }
|
.feat h3 { font-size: 21px; margin-bottom: 8px; }
|
||||||
.feat p { margin: 0; color: var(--muted); font-size: 16px; }
|
.feat p { margin: 0; color: var(--muted); font-size: 16px; }
|
||||||
|
|
||||||
/* pixel icon */
|
/* pixel icon — plain (no bevel) */
|
||||||
.picon {
|
.picon {
|
||||||
width: 52px; height: 52px; flex-shrink: 0;
|
width: 52px; height: 52px; flex-shrink: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -530,12 +530,17 @@ section { position: relative; z-index: 1; }
|
|||||||
grid-template-rows: repeat(7, 1fr);
|
grid-template-rows: repeat(7, 1fr);
|
||||||
background: var(--slot);
|
background: var(--slot);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
box-shadow: inset 2px 2px 0 var(--bevel-lo), inset -2px -2px 0 var(--bevel-hi);
|
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
.picon i { background: transparent; }
|
.picon i { background: transparent; }
|
||||||
.picon i.on { background: var(--accent); }
|
.picon i.on { background: var(--accent); }
|
||||||
.picon i.go { background: var(--gold); }
|
.picon i.go { background: var(--gold); }
|
||||||
|
/* custom image icon (PixelIcon `img` prop) */
|
||||||
|
.picon-img {
|
||||||
|
width: 52px; height: 52px; flex-shrink: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
HOW TO JOIN — steps
|
HOW TO JOIN — steps
|
||||||
|
|||||||
@@ -61,11 +61,15 @@ script only if the font set changes (keep families in sync with `main.css`
|
|||||||
|
|
||||||
## Languages (i18n)
|
## Languages (i18n)
|
||||||
|
|
||||||
Three locales, static, build-time — **English** (default, `/`),
|
Three locales, static, build-time — **Spanish** (default, `/`),
|
||||||
**Spanish** (`/es/`), **Euskera** (`/eu/`).
|
**English** (`/en/`), **Euskera** (`/eu/`).
|
||||||
|
|
||||||
- One template `src/pages/[...lang].astro` with `getStaticPaths` emits all three
|
- One template `src/pages/[...lang].astro` with `getStaticPaths` emits all three
|
||||||
routes (`/`, `/es/`, `/eu/`). No runtime/JS routing.
|
routes (`/`, `/en/`, `/eu/`). No runtime/JS routing. The default locale lives at
|
||||||
|
the bare root: its `getStaticPaths` entry is `{ lang: undefined }` → `locale:
|
||||||
|
"es"`; en/eu get an explicit prefix. The `LANG_*_PATH` maps in `ui.ts` mirror
|
||||||
|
this (es → no prefix). Changing the default = swap which locale is `undefined`
|
||||||
|
in all four pages' `getStaticPaths` + the `LANG_*_PATH` maps.
|
||||||
- Translatable copy lives in `src/i18n/ui.ts` (`ui[locale]`), keyed identically
|
- Translatable copy lives in `src/i18n/ui.ts` (`ui[locale]`), keyed identically
|
||||||
across locales. `site.ts` holds only language-neutral config (URLs, theme,
|
across locales. `site.ts` holds only language-neutral config (URLs, theme,
|
||||||
launcher files, stat/feature *structure*).
|
launcher files, stat/feature *structure*).
|
||||||
@@ -103,6 +107,30 @@ Page links to fixed names so they survive launcher version bumps:
|
|||||||
sync** between `site.ts` and the script. Mounted so they resolve at
|
sync** between `site.ts` and the script. Mounted so they resolve at
|
||||||
`/launcher/latest/…` (see Caddy `10-static.caddy`).
|
`/launcher/latest/…` (see Caddy `10-static.caddy`).
|
||||||
|
|
||||||
|
## Assets — where to drop images
|
||||||
|
|
||||||
|
All static images live under `landing/public/` and are referenced by an absolute
|
||||||
|
`/path` (Astro copies `public/` to the site root verbatim). After changing any,
|
||||||
|
rebuild the landing (`pnpm run build`).
|
||||||
|
|
||||||
|
- **Header / hero logo** — `landing/public/logo.png` (current 707×148). Referenced
|
||||||
|
as `/logo.png` in the hero (`[...lang].astro`, the `.hero-logo img`). Replace the
|
||||||
|
file; keep a wide transparent PNG. Update the `width`/`height` attrs if the
|
||||||
|
aspect changes.
|
||||||
|
- **Favicon** — `landing/public/favicon.png` (square, ~175×175). Referenced as
|
||||||
|
`/favicon.png` in the `<link rel="icon">` of all four pages
|
||||||
|
(`[...lang].astro`, `register`, `account`, `fjord`). Replace the file.
|
||||||
|
- **Server-status icon** — NOT a file you place here. The ServerCard pulls the
|
||||||
|
live server icon over the SLP query (`ServerCard.astro`, the favicon data-URI),
|
||||||
|
falling back to the `Creeper.astro` SVG. To change it, set `server-icon.png` on
|
||||||
|
the Minecraft server (64×64), not in the landing.
|
||||||
|
- **Features icons** — two options (`PixelIcon.astro`, fed by `site.ts` `features[]`):
|
||||||
|
1. *Procedural glyph* (default): pick `glyph ∈ shield|diamond|orb|heart` and
|
||||||
|
`gold` per feature in `site.ts`. Add a glyph by editing the 7×7 `GLYPHS` map.
|
||||||
|
2. *Custom image*: drop a file in `landing/public/icons/<name>.(png|svg)` and add
|
||||||
|
`img: "/icons/<name>.png"` to that feature's entry in `site.ts` `features[]`.
|
||||||
|
`PixelIcon` renders the image (`.picon-img`) instead of the glyph.
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- [x] Vendor fonts (`tooling/fetch-fonts.sh`) → `public/fonts/`
|
- [x] Vendor fonts (`tooling/fetch-fonts.sh`) → `public/fonts/`
|
||||||
|
|||||||
Reference in New Issue
Block a user