import { useState, type FormEvent } from 'react';
import { KeyRound, Languages } from 'lucide-react';
import { useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { Button, Field, Input } from '../components/ui';
import { MinecraftIcon } from '../components/minecraft-icon';
export function LoginPage() {
const { t, locale, setLocale } = useI18n();
const { login, setup, session } = useServer();
const setupRequired = session?.bootstrapRequired === true;
const [bootstrapToken, setBootstrapToken] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const [totp, setTotp] = useState('');
const [useRecoveryCode, setUseRecoveryCode] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
const submit = async (event: FormEvent) => {
event.preventDefault();
setBusy(true);
try {
if (setupRequired) {
if (password.length < 12) throw new Error(t('auth.passwordHint'));
if (password !== passwordConfirmation) throw new Error(t('auth.passwordMismatch'));
await setup(bootstrapToken.trim(), username.trim(), password);
} else {
await login(username.trim(), password, totp.trim() || undefined, useRecoveryCode);
}
setError(undefined);
}
catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
finally { setBusy(false); }
};
return <main className="login-page">
<div className="login-page__landscape" aria-hidden="true">
<div className="login-page__sun" />
<div className="login-page__terrain login-page__terrain--far" />
<div className="login-page__terrain login-page__terrain--near" />
</div>
<button className="language-button login-language" onClick={() => setLocale(locale === 'zh-CN' ? 'en-US' : 'zh-CN')}>
<Languages size={16} />{locale === 'zh-CN' ? 'English' : '简体中文'}
</button>
<section className="login-card">
<div className="login-brand">
<div className="brand__mark brand__mark--minecraft" aria-hidden="true"><MinecraftIcon name="grass" size={32} /></div>
<span>{t('app.name')}</span>
</div>
<div className="login-icon" aria-hidden="true"><MinecraftIcon name="shield" size={32} /></div>
<h1>{setupRequired ? t('auth.setupTitle') : t('auth.title')}</h1>
<p>{setupRequired ? t('auth.setupDescription') : t('auth.description')}</p>
{setupRequired && <div className="bootstrap-note"><KeyRound />{t('auth.bootstrap')}</div>}
<form className="form-stack" onSubmit={submit}>
{setupRequired && <Field label={t('auth.bootstrapToken')}>
<Input autoComplete="off" value={bootstrapToken} onChange={(event) => setBootstrapToken(event.target.value)} required />
</Field>}
<Field label={t('auth.username')}>
<Input autoComplete="username" minLength={3} value={username} onChange={(event) => setUsername(event.target.value)} required />
</Field>
<Field label={t('auth.password')} hint={setupRequired ? t('auth.passwordHint') : undefined}>
<Input type="password" autoComplete={setupRequired ? 'new-password' : 'current-password'}
minLength={setupRequired ? 12 : undefined} value={password} onChange={(event) => setPassword(event.target.value)} required />
</Field>
{setupRequired && <Field label={t('auth.passwordConfirm')}>
<Input type="password" autoComplete="new-password" minLength={12} value={passwordConfirmation}
onChange={(event) => setPasswordConfirmation(event.target.value)} required />
</Field>}
{!setupRequired && <>
<Field label={useRecoveryCode ? t('auth.recoveryCode') : t('auth.totp')}>
<Input inputMode={useRecoveryCode ? 'text' : 'numeric'} autoComplete="one-time-code"
pattern={useRecoveryCode ? undefined : '[0-9]{6}'} value={totp}
onChange={(event) => setTotp(useRecoveryCode ? event.target.value : event.target.value.replace(/\D/g, '').slice(0, 6))} />
</Field>
<button type="button" className="auth-mode-toggle" onClick={() => { setUseRecoveryCode((current) => !current); setTotp(''); }}>
{useRecoveryCode ? t('auth.useTotp') : t('auth.useRecovery')}
</button>
</>}
{error && <div className="form-error" role="alert">{error}</div>}
<Button type="submit" disabled={busy || !username || !password || (setupRequired && (!bootstrapToken.trim() || !passwordConfirmation))}>
<MinecraftIcon name="shield" size={20} />{setupRequired ? t('auth.createOwner') : t('auth.signIn')}
</Button>
</form>
</section>
</main>;
}
import { useState, type FormEvent } from 'react';
import { KeyRound, Languages } from 'lucide-react';
import { useServer } from '../context/server-context';
import { useI18n } from '../lib/i18n';
import { Button, Field, Input } from '../components/ui';
import { MinecraftIcon } from '../components/minecraft-icon';
export function LoginPage() {
const { t, locale, setLocale } = useI18n();
const { login, setup, session } = useServer();
const setupRequired = session?.bootstrapRequired === true;
const [bootstrapToken, setBootstrapToken] = useState('');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const [totp, setTotp] = useState('');
const [useRecoveryCode, setUseRecoveryCode] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
const submit = async (event: FormEvent) => {
event.preventDefault();
setBusy(true);
try {
if (setupRequired) {
if (password.length < 12) throw new Error(t('auth.passwordHint'));
if (password !== passwordConfirmation) throw new Error(t('auth.passwordMismatch'));
await setup(bootstrapToken.trim(), username.trim(), password);
} else {
await login(username.trim(), password, totp.trim() || undefined, useRecoveryCode);
}
setError(undefined);
}
catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
finally { setBusy(false); }
};
return <main className="login-page">
<div className="login-page__landscape" aria-hidden="true">
<div className="login-page__sun" />
<div className="login-page__terrain login-page__terrain--far" />
<div className="login-page__terrain login-page__terrain--near" />
</div>
<button className="language-button login-language" onClick={() => setLocale(locale === 'zh-CN' ? 'en-US' : 'zh-CN')}>
<Languages size={16} />{locale === 'zh-CN' ? 'English' : '简体中文'}
</button>
<section className="login-card">
<div className="login-brand">
<div className="brand__mark brand__mark--minecraft" aria-hidden="true"><MinecraftIcon name="grass" size={32} /></div>
<span>{t('app.name')}</span>
</div>
<div className="login-icon" aria-hidden="true"><MinecraftIcon name="shield" size={32} /></div>
<h1>{setupRequired ? t('auth.setupTitle') : t('auth.title')}</h1>
<p>{setupRequired ? t('auth.setupDescription') : t('auth.description')}</p>
{setupRequired && <div className="bootstrap-note"><KeyRound />{t('auth.bootstrap')}</div>}
<form className="form-stack" onSubmit={submit}>
{setupRequired && <Field label={t('auth.bootstrapToken')}>
<Input autoComplete="off" value={bootstrapToken} onChange={(event) => setBootstrapToken(event.target.value)} required />
</Field>}
<Field label={t('auth.username')}>
<Input autoComplete="username" minLength={3} value={username} onChange={(event) => setUsername(event.target.value)} required />
</Field>
<Field label={t('auth.password')} hint={setupRequired ? t('auth.passwordHint') : undefined}>
<Input type="password" autoComplete={setupRequired ? 'new-password' : 'current-password'}
minLength={setupRequired ? 12 : undefined} value={password} onChange={(event) => setPassword(event.target.value)} required />
</Field>
{setupRequired && <Field label={t('auth.passwordConfirm')}>
<Input type="password" autoComplete="new-password" minLength={12} value={passwordConfirmation}
onChange={(event) => setPasswordConfirmation(event.target.value)} required />
</Field>}
{!setupRequired && <>
<Field label={useRecoveryCode ? t('auth.recoveryCode') : t('auth.totp')}>
<Input inputMode={useRecoveryCode ? 'text' : 'numeric'} autoComplete="one-time-code"
pattern={useRecoveryCode ? undefined : '[0-9]{6}'} value={totp}
onChange={(event) => setTotp(useRecoveryCode ? event.target.value : event.target.value.replace(/\D/g, '').slice(0, 6))} />
</Field>
<button type="button" className="auth-mode-toggle" onClick={() => { setUseRecoveryCode((current) => !current); setTotp(''); }}>
{useRecoveryCode ? t('auth.useTotp') : t('auth.useRecovery')}
</button>
</>}
{error && <div className="form-error" role="alert">{error}</div>}
<Button type="submit" disabled={busy || !username || !password || (setupRequired && (!bootstrapToken.trim() || !passwordConfirmation))}>
<MinecraftIcon name="shield" size={20} />{setupRequired ? t('auth.createOwner') : t('auth.signIn')}
</Button>
</form>
</section>
</main>;
}