feat(landing): join-flow rework, player rosters, account page, mod list
Execute plan/18 (WS1-6) + plan/17.
- WS1: homepage join = 3 steps off packwiz (register -> UlicraftLauncher
-> join); per-OS downloads from a synced launcher-manifest.json (schema +
example committed); Fjord moved to its own /fjord page. Drop packwizUrl.
- WS2/3: mc-status gains an isolated /players endpoint (admin login ->
Drasl GET /players, own client+TTL+token cache, never blocks the status
path); online + registered rosters render shared name+avatar tiles;
AVATAR_MODE env (default headiso).
- WS4: /account skin CRUD via browser-direct Drasl (login -> upload ->
reset), in-memory token, players[0]; register relinks "Manage it here".
- WS5: footer link to status.${BASE_DOMAIN} in every footer.
- WS6: tooling/build-modlist.py generates mods.json + extracted logos from
the distribution forgemods; ModList.astro renders a flat list; stat tile
fed from the count.
Manifest/mods loaders read at build with a process.cwd() fallback (the
import.meta.url-only path does not resolve in Astro's bundled build).
New env: AVATAR_MODE, DRASL_ADMIN_USERNAME, DRASL_ADMIN_PASSWORD (mc-status
only). Generated mods.json / launcher-manifest.json / logos are gitignored;
.example.json files document the shapes. Build: 12 pages; mc-status builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
096ef82420
commit
6c7e259fcb
24 files changed
+2319
-117
No files matched your search
@@ -1,10 +1,12 @@
|
||||
---
|
||||
import { site, LITERALS, HEAD_FONTS } from "../data/site";
|
||||
import { ui, LANGS, LANG_LABEL, LANG_PATH, type Lang } from "../i18n/ui";
|
||||
import { ui, LANGS, LANG_LABEL, LANG_PATH, LANG_REGISTER_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";
|
||||
import "../styles/main.css";
|
||||
|
||||
// One static page per locale: en at "/", es at "/es/", eu at "/eu/".
|
||||
@@ -18,9 +20,19 @@ export function getStaticPaths() {
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const t = ui[locale];
|
||||
const { theme, launcher } = site;
|
||||
const { theme, launcherManifest } = site;
|
||||
const headFont = HEAD_FONTS[theme.headFont] ?? HEAD_FONTS.pixelify;
|
||||
const launcherName = launcher.name;
|
||||
const launcherName = launcherManifest.product;
|
||||
const registerPath = LANG_REGISTER_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).
|
||||
const OS_LABELS: Record<string, string> = { windows: "Windows", macos: "macOS", linux: "Linux" };
|
||||
const launcherBuilds = launcherManifest.builds.map((b) => ({
|
||||
label: b.label ?? `${OS_LABELS[b.os] ?? b.os}`,
|
||||
hint: `${b.arch} · ${b.format}`,
|
||||
url: b.url,
|
||||
}));
|
||||
|
||||
// Build-time floating dust bits (index-derived, no runtime RNG).
|
||||
const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
@@ -62,7 +74,9 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
</a>
|
||||
<nav class="nav-links">
|
||||
<a href="#status">{t.nav.status}</a>
|
||||
<a href="#members">{t.nav.members}</a>
|
||||
<a href="#features">{t.nav.features}</a>
|
||||
<a href="#mods">{t.nav.mods}</a>
|
||||
<a href="#join">{t.nav.join}</a>
|
||||
</nav>
|
||||
<span class="nav-spacer"></span>
|
||||
@@ -143,8 +157,22 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
</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" style="background: var(--bg-2)">
|
||||
<section id="features" class="pad">
|
||||
<div class="wrap">
|
||||
<div class="sec-head reveal">
|
||||
<span class="eyebrow">{t.features.eyebrow}</span>
|
||||
@@ -165,6 +193,20 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
</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 -->
|
||||
<section id="join" class="pad">
|
||||
<div class="wrap">
|
||||
@@ -180,15 +222,10 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
<div class="num">1</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s1.kicker}</span>
|
||||
<h3>{t.join.s1.h.replace("{name}", launcherName)}</h3>
|
||||
<p>{t.join.s1.p.replace(/\{name\}/g, launcherName)}</p>
|
||||
<div class="actions">
|
||||
{launcher.downloads.map((d) => (
|
||||
<a class="mc-btn sm" href={`${launcher.base}/${d.file}`}>
|
||||
{d.os} <span class="hint">· {d.hint}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<h3>{t.join.s1.h}</h3>
|
||||
<p>
|
||||
{t.join.s1.pa} <a href={registerPath}>{t.join.s1.registerLink}</a> {t.join.s1.pb}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,16 +234,17 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
<div class="num">2</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s2.kicker}</span>
|
||||
<h3>{t.join.s2.h}</h3>
|
||||
<p>
|
||||
{t.join.s2.pa} <strong style="color:var(--text)">{LITERALS.authlib}</strong> {t.join.s2.pb}
|
||||
</p>
|
||||
<div class="actions" style="margin-bottom:14px">
|
||||
<CopyChip value={site.authlibUrl} variant="url" label={t.join.s2.copy} copiedLabel={t.copied} />
|
||||
</div>
|
||||
<p>
|
||||
{t.join.s2.registerPre} <a href={site.authUrl}>{site.authUrl}</a>.
|
||||
</p>
|
||||
<h3>{t.join.s2.h.replace("{name}", launcherName)}</h3>
|
||||
<p>{t.join.s2.p.replace(/\{name\}/g, launcherName)}</p>
|
||||
{launcherBuilds.length > 0 && (
|
||||
<div class="actions">
|
||||
{launcherBuilds.map((b) => (
|
||||
<a class="mc-btn sm" href={b.url}>
|
||||
{b.label} <span class="hint">· {b.hint}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -216,24 +254,11 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s3.kicker}</span>
|
||||
<h3>{t.join.s3.h}</h3>
|
||||
<p>{t.join.s3.p}</p>
|
||||
<div class="actions">
|
||||
<CopyChip value={site.packwizUrl} variant="url" label={t.join.s2.copy} copiedLabel={t.copied} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4 -->
|
||||
<div class="step reveal">
|
||||
<div class="num">4</div>
|
||||
<div class="body">
|
||||
<span class="kicker">{t.join.s4.kicker}</span>
|
||||
<h3>{t.join.s4.h}</h3>
|
||||
<p>
|
||||
{t.join.s4.pa} <kbd>{LITERALS.multiplayer}</kbd> → <kbd>{LITERALS.addServer}</kbd>{t.join.s4.pb}
|
||||
{t.join.s3.pa} <kbd>{LITERALS.multiplayer}</kbd> → <kbd>{LITERALS.addServer}</kbd>{t.join.s3.pb}
|
||||
</p>
|
||||
<div class="actions">
|
||||
<CopyChip value={site.serverAddress} variant="ip" label={t.join.s4.copy} copiedLabel={t.copied} />
|
||||
<CopyChip value={site.serverAddress} variant="ip" label={t.join.s3.copy} copiedLabel={t.copied} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,6 +276,7 @@ const dustBits = Array.from({ length: 16 }, (_, i) => {
|
||||
</a>
|
||||
<div class="footer-links">
|
||||
<a class="mc-btn primary sm" href="#join">{t.footer.join}</a>
|
||||
<a class="mc-btn sm" href={site.statusUrl} target="_blank" rel="noopener">{t.footer.status}</a>
|
||||
</div>
|
||||
<p class="disc">
|
||||
{t.footer.disc} {site.name} · {site.baseDomain}
|
||||
|
||||
Reference in new issue
Block a user