Initial I Want to Heal app
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
loginAccount,
|
||||
registerAccount,
|
||||
type AuthSession,
|
||||
} from '../profile'
|
||||
import {
|
||||
createOfflineCharacter,
|
||||
hasOfflineCharacter,
|
||||
resumeOfflineCharacter,
|
||||
selectOnlineMode,
|
||||
} from '../gameRepository'
|
||||
|
||||
type Props = {
|
||||
onAuthenticated: (session: AuthSession) => void
|
||||
serverMessage?: string
|
||||
}
|
||||
|
||||
export function AuthScreen({ onAuthenticated, serverMessage = '' }: Props) {
|
||||
const [mode, setMode] = useState<'login' | 'register'>('login')
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [characterName, setCharacterName] = useState('')
|
||||
const [offlineName, setOfflineName] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const offlineCharacterExists = hasOfflineCharacter()
|
||||
|
||||
async function submit(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
setBusy(true)
|
||||
setMessage('')
|
||||
try {
|
||||
selectOnlineMode()
|
||||
const session = mode === 'login'
|
||||
? await loginAccount(username, password)
|
||||
: await registerAccount(username, password, characterName)
|
||||
onAuthenticated(session)
|
||||
} catch (reason) {
|
||||
setMessage(reason instanceof Error ? reason.message : 'Unable to authenticate.')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
function beginOffline() {
|
||||
setMessage('')
|
||||
try {
|
||||
onAuthenticated(createOfflineCharacter(offlineName))
|
||||
} catch (reason) {
|
||||
setMessage(reason instanceof Error ? reason.message : 'Unable to create an offline character.')
|
||||
}
|
||||
}
|
||||
|
||||
function resumeOffline() {
|
||||
const session = resumeOfflineCharacter()
|
||||
if (session) onAuthenticated(session)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="auth-shell">
|
||||
<section className="auth-panel">
|
||||
<div className="auth-brand">
|
||||
<p className="eyebrow">Healer RPG</p>
|
||||
<h1>I want to Heal</h1>
|
||||
<p>
|
||||
Build your healer, master each dungeon, and compete for the most
|
||||
efficient clears.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="auth-card">
|
||||
<div className="auth-tabs">
|
||||
<button
|
||||
className={mode === 'login' ? 'selected' : ''}
|
||||
onClick={() => {
|
||||
setMode('login')
|
||||
setMessage('')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
className={mode === 'register' ? 'selected' : ''}
|
||||
onClick={() => {
|
||||
setMode('register')
|
||||
setMessage('')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Create Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
autoComplete="username"
|
||||
maxLength={20}
|
||||
minLength={3}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
pattern="[A-Za-z0-9_]+"
|
||||
required
|
||||
value={username}
|
||||
/>
|
||||
</label>
|
||||
{mode === 'register' && (
|
||||
<label>
|
||||
Character Name
|
||||
<input
|
||||
autoComplete="nickname"
|
||||
maxLength={20}
|
||||
minLength={2}
|
||||
onChange={(event) => setCharacterName(event.target.value)}
|
||||
required
|
||||
value={characterName}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
|
||||
maxLength={128}
|
||||
minLength={10}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-button" disabled={busy} type="submit">
|
||||
{busy
|
||||
? 'Working...'
|
||||
: mode === 'login'
|
||||
? 'Enter Chronicle'
|
||||
: 'Begin Adventure'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className={`auth-message ${message ? 'error' : ''}`}>
|
||||
{message || serverMessage || (
|
||||
mode === 'register'
|
||||
? 'The first account keeps the current local character and save.'
|
||||
: 'Sign in to continue your character.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="offline-divider"><span>or</span></div>
|
||||
|
||||
<section className="offline-entry">
|
||||
<div>
|
||||
<p className="eyebrow">Local Save</p>
|
||||
<h2>Play Offline</h2>
|
||||
<p>
|
||||
No account or connection required. Offline progress stays on
|
||||
this device and is excluded from online leaderboards.
|
||||
</p>
|
||||
</div>
|
||||
{offlineCharacterExists && (
|
||||
<button
|
||||
className="offline-resume-button"
|
||||
onClick={resumeOffline}
|
||||
type="button"
|
||||
>
|
||||
Continue Offline Character
|
||||
</button>
|
||||
)}
|
||||
<label>
|
||||
{offlineCharacterExists ? 'New Character Name' : 'Character Name'}
|
||||
<input
|
||||
maxLength={20}
|
||||
minLength={2}
|
||||
onChange={(event) => setOfflineName(event.target.value)}
|
||||
placeholder="Mira"
|
||||
value={offlineName}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="text-button offline-new-button"
|
||||
onClick={beginOffline}
|
||||
type="button"
|
||||
>
|
||||
{offlineCharacterExists ? 'Replace Offline Character' : 'Begin Offline Adventure'}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user