The home page header already used ulicraft-logo-mini.svg; apply the same brand mark to the register, account, and fjord page headers (and footers), replacing the leftover Creeper pixel-art. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
320 lines
11 KiB
Plaintext
320 lines
11 KiB
Plaintext
---
|
|
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 "../../styles/main.css";
|
|
|
|
// One static register page per locale: /register, /es/register, /eu/register.
|
|
export function getStaticPaths() {
|
|
return [
|
|
{ params: { lang: undefined }, props: { locale: "es" as Lang } },
|
|
{ params: { lang: "en" }, props: { locale: "en" as Lang } },
|
|
{ params: { lang: "eu" }, props: { locale: "eu" as Lang } },
|
|
];
|
|
}
|
|
|
|
const { locale } = Astro.props;
|
|
const t = ui[locale];
|
|
const r = t.register;
|
|
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];
|
|
// 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).
|
|
const clientCfg = {
|
|
apiUrl: site.draslApiUrl,
|
|
showInvite,
|
|
submit: r.submit,
|
|
submitting: r.submitting,
|
|
skinUpload: r.skinUpload,
|
|
skinUploading: r.skinUploading,
|
|
skinOk: r.skinOk,
|
|
successBody: r.successBody,
|
|
errFields: r.errFields,
|
|
errSkinType: r.errSkinType,
|
|
errNetwork: r.errNetwork,
|
|
};
|
|
---
|
|
|
|
<!doctype html>
|
|
<html
|
|
lang={t.htmlLang}
|
|
data-mood={theme.mood}
|
|
data-head={theme.headFont}
|
|
style={`--font-head: ${headFont}`}
|
|
>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>{site.name} — {r.title}</title>
|
|
<meta name="description" content={r.lead} />
|
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
|
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={LANG_REGISTER_PATH[l]} />)}
|
|
</head>
|
|
<body>
|
|
<!-- NAV -->
|
|
<header class="nav">
|
|
<div class="wrap nav-in">
|
|
<a class="brand" href={homePath}>
|
|
<img class="brand-logo" src="/ulicraft-logo-mini.svg" alt="" width="34" height="34" />
|
|
<span class="name">ULICRAFT</span>
|
|
</a>
|
|
<nav class="nav-links">
|
|
<a href={homePath}>{r.navHome}</a>
|
|
</nav>
|
|
<span class="nav-spacer"></span>
|
|
<div class="lang-switch" aria-label="Language">
|
|
{LANGS.map((l) => (
|
|
<a
|
|
href={LANG_REGISTER_PATH[l]}
|
|
class:list={[l === locale && "on"]}
|
|
aria-current={l === locale ? "true" : undefined}
|
|
>{LANG_LABEL[l]}</a>
|
|
))}
|
|
</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>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="top">
|
|
<section class="pad">
|
|
<div class="reg-wrap">
|
|
<div class="reg-card">
|
|
<span class="eyebrow">{r.eyebrow}</span>
|
|
<h1>{r.title}</h1>
|
|
<p class="lead">{r.lead}</p>
|
|
|
|
<!-- REGISTER FORM -->
|
|
<form class="reg-form" id="reg-form" novalidate>
|
|
<div class="reg-field">
|
|
<label for="reg-username">{r.usernameLabel}</label>
|
|
<input class="reg-input" id="reg-username" name="username"
|
|
type="text" autocomplete="username" placeholder={r.usernamePlaceholder} required />
|
|
</div>
|
|
<div class="reg-field">
|
|
<label for="reg-password">{r.passwordLabel}</label>
|
|
<input class="reg-input" id="reg-password" name="password"
|
|
type="password" autocomplete="new-password" placeholder={r.passwordPlaceholder} required />
|
|
</div>
|
|
{showInvite && (
|
|
<div class="reg-field">
|
|
<label for="reg-invite">{r.inviteLabel}</label>
|
|
<input class="reg-input" id="reg-invite" name="invite"
|
|
type="text" placeholder={r.invitePlaceholder} required />
|
|
<span class="hint">{r.inviteHint}</span>
|
|
</div>
|
|
)}
|
|
<div class="reg-msg err" id="reg-error" role="alert" hidden></div>
|
|
<button class="mc-btn primary" id="reg-submit" type="submit">{r.submit}</button>
|
|
</form>
|
|
|
|
<p class="reg-foot">
|
|
{r.haveAccount} <a href={LANG_ACCOUNT_PATH[locale]}>{r.loginLink}</a>
|
|
</p>
|
|
|
|
<!-- SUCCESS + SKIN -->
|
|
<div class="reg-success" id="reg-success" hidden>
|
|
<div>
|
|
<span class="eyebrow" style="color:var(--accent)">{r.successTitle}</span>
|
|
<p class="lead" id="reg-success-body" style="margin-top:10px"></p>
|
|
</div>
|
|
<div class="reg-field">
|
|
<label>{r.uuidLabel}</label>
|
|
<div class="reg-uuid" id="reg-uuid"></div>
|
|
</div>
|
|
|
|
<div class="reg-skin" id="reg-skin">
|
|
<div>
|
|
<h3>{r.skinTitle}</h3>
|
|
<p class="lead">{r.skinLead}</p>
|
|
</div>
|
|
<div class="reg-field">
|
|
<label class="mc-btn sm" for="reg-skin-file" style="align-self:flex-start">{r.skinChoose}</label>
|
|
<input id="reg-skin-file" type="file" accept="image/png" hidden />
|
|
<span class="reg-file-name" id="reg-skin-name"></span>
|
|
</div>
|
|
<div class="reg-field">
|
|
<label>{r.modelLabel}</label>
|
|
<div class="reg-models">
|
|
<label><input type="radio" name="skin-model" value="classic" checked /> {r.modelClassic}</label>
|
|
<label><input type="radio" name="skin-model" value="slim" /> {r.modelSlim}</label>
|
|
</div>
|
|
</div>
|
|
<div class="reg-msg" id="reg-skin-msg" role="status" hidden></div>
|
|
<button class="mc-btn" id="reg-skin-upload" type="button">{r.skinUpload}</button>
|
|
</div>
|
|
|
|
<a class="mc-btn primary" href={`${homePath}#join`}>{r.finish}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="footer">
|
|
<div class="wrap footer-in">
|
|
<a class="brand" href={homePath}>
|
|
<img class="brand-logo" src="/ulicraft-logo-mini.svg" alt="" width="34" height="34" />
|
|
<span class="name">ULICRAFT</span>
|
|
</a>
|
|
<div class="footer-links">
|
|
<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}
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script define:vars={{ cfg: clientCfg }}>
|
|
const $ = (id) => document.getElementById(id);
|
|
const form = $("reg-form");
|
|
const errBox = $("reg-error");
|
|
const submitBtn = $("reg-submit");
|
|
const successBox = $("reg-success");
|
|
const headers = { "Content-Type": "application/json" };
|
|
|
|
let apiToken = null;
|
|
let player = null;
|
|
|
|
const showErr = (box, msg) => {
|
|
box.textContent = msg;
|
|
box.hidden = false;
|
|
};
|
|
const apiMessage = async (res, fallback) => {
|
|
try {
|
|
const data = await res.json();
|
|
return data && data.message ? data.message : fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
};
|
|
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
errBox.hidden = true;
|
|
const username = $("reg-username").value.trim();
|
|
const password = $("reg-password").value;
|
|
const inviteEl = $("reg-invite");
|
|
const invite = inviteEl ? inviteEl.value.trim() : null;
|
|
if (!username || !password || (cfg.showInvite && !invite)) {
|
|
showErr(errBox, cfg.errFields);
|
|
return;
|
|
}
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = cfg.submitting;
|
|
try {
|
|
const body = { username, password, playerName: username };
|
|
if (invite) body.inviteCode = invite;
|
|
let res = await fetch(`${cfg.apiUrl}/users`, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) {
|
|
showErr(errBox, await apiMessage(res, cfg.errNetwork));
|
|
return;
|
|
}
|
|
// Log in to get an API token for the (optional) skin upload.
|
|
res = await fetch(`${cfg.apiUrl}/login`, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
if (!res.ok) {
|
|
showErr(errBox, await apiMessage(res, cfg.errNetwork));
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
apiToken = data.apiToken;
|
|
player = data.user.players[0];
|
|
form.hidden = true;
|
|
$("reg-uuid").textContent = player.uuid;
|
|
$("reg-success-body").textContent = cfg.successBody.replace("{name}", player.name);
|
|
successBox.hidden = false;
|
|
} catch {
|
|
showErr(errBox, cfg.errNetwork);
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = cfg.submit;
|
|
}
|
|
});
|
|
|
|
// ---- Skin upload ----
|
|
const fileInput = $("reg-skin-file");
|
|
const fileName = $("reg-skin-name");
|
|
const skinMsg = $("reg-skin-msg");
|
|
const skinBtn = $("reg-skin-upload");
|
|
|
|
fileInput.addEventListener("change", () => {
|
|
skinMsg.hidden = true;
|
|
fileName.textContent = fileInput.files && fileInput.files[0] ? fileInput.files[0].name : "";
|
|
});
|
|
|
|
const readBase64 = (file) =>
|
|
new Promise((resolve, reject) => {
|
|
const fr = new FileReader();
|
|
fr.onload = () => resolve(String(fr.result).split(",")[1]);
|
|
fr.onerror = reject;
|
|
fr.readAsDataURL(file);
|
|
});
|
|
|
|
skinBtn.addEventListener("click", async () => {
|
|
skinMsg.hidden = true;
|
|
skinMsg.classList.remove("ok", "err");
|
|
const file = fileInput.files && fileInput.files[0];
|
|
if (!file) {
|
|
skinMsg.classList.add("err");
|
|
showErr(skinMsg, cfg.errSkinType);
|
|
return;
|
|
}
|
|
if (file.type !== "image/png") {
|
|
skinMsg.classList.add("err");
|
|
showErr(skinMsg, cfg.errSkinType);
|
|
return;
|
|
}
|
|
const model = document.querySelector('input[name="skin-model"]:checked').value;
|
|
skinBtn.disabled = true;
|
|
skinBtn.textContent = cfg.skinUploading;
|
|
try {
|
|
const skinBase64 = await readBase64(file);
|
|
const res = await fetch(`${cfg.apiUrl}/players/${player.uuid}`, {
|
|
method: "PATCH",
|
|
headers: { ...headers, Authorization: `Bearer ${apiToken}` },
|
|
body: JSON.stringify({ skinBase64, skinModel: model }),
|
|
});
|
|
if (!res.ok) {
|
|
skinMsg.classList.add("err");
|
|
showErr(skinMsg, await apiMessage(res, cfg.errNetwork));
|
|
return;
|
|
}
|
|
skinMsg.classList.add("ok");
|
|
showErr(skinMsg, cfg.skinOk);
|
|
} catch {
|
|
skinMsg.classList.add("err");
|
|
showErr(skinMsg, cfg.errNetwork);
|
|
} finally {
|
|
skinBtn.disabled = false;
|
|
skinBtn.textContent = cfg.skinUpload;
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|