mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
🆙 Added Step 3 for UI login registration
This commit is contained in:
@@ -66,6 +66,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
||||
const right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', ''));
|
||||
const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
|
||||
const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register');
|
||||
const roomTemplatesUrl = GetConfigurationValue<string>('login.room_templates.endpoint', '/api/auth/room-templates');
|
||||
const forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password');
|
||||
const turnstileSiteKey = GetConfigurationValue<string>('login.turnstile.sitekey', '');
|
||||
const rawTurnstileEnabled = GetConfigurationValue<unknown>('login.turnstile.enabled', false);
|
||||
@@ -316,7 +317,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
||||
}
|
||||
}, [ checkUsernameUrl, postJson ]);
|
||||
|
||||
const handleRegisterSubmit = useCallback(async (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; }, onDialogReset: () => void) =>
|
||||
const handleRegisterSubmit = useCallback(async (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; templateId: number | null; }, onDialogReset: () => void) =>
|
||||
{
|
||||
if(turnstileEnabled && !body.turnstileToken)
|
||||
{
|
||||
@@ -336,6 +337,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
||||
password: body.password,
|
||||
figure: body.figure,
|
||||
gender: body.gender,
|
||||
templateId: body.templateId ?? undefined,
|
||||
turnstileToken: turnstileEnabled ? body.turnstileToken : undefined
|
||||
});
|
||||
|
||||
@@ -491,6 +493,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
||||
onCheckUsername={ checkUsernameAvailable }
|
||||
onCheckServer={ checkServerReachable }
|
||||
imagingUrl={ imagingUrl }
|
||||
roomTemplatesUrl={ roomTemplatesUrl }
|
||||
submitting={ submitting }
|
||||
error={ error }
|
||||
info={ info }
|
||||
@@ -524,14 +527,17 @@ interface DialogSharedProps
|
||||
|
||||
interface RegisterDialogProps extends DialogSharedProps
|
||||
{
|
||||
onSubmit: (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; }, onDialogReset: () => void) => void;
|
||||
onSubmit: (body: { username: string; email: string; password: string; figure: string; gender: string; turnstileToken: string; templateId: number | null; }, onDialogReset: () => void) => void;
|
||||
onCheckEmail: (email: string) => Promise<{ available: boolean; error?: string }>;
|
||||
onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>;
|
||||
onCheckServer: () => Promise<boolean>;
|
||||
imagingUrl: string;
|
||||
roomTemplatesUrl: string;
|
||||
}
|
||||
|
||||
type RegisterStep = 'credentials' | 'avatar';
|
||||
type RegisterStep = 'credentials' | 'avatar' | 'room';
|
||||
|
||||
interface RoomTemplate { templateId: number; title: string; description: string; thumbnail: string; }
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
@@ -642,7 +648,7 @@ const buildPartPreviewUrl = (
|
||||
|
||||
const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
{
|
||||
const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, imagingUrl, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props;
|
||||
const { onCancel, onSubmit, onCheckEmail, onCheckUsername, onCheckServer, imagingUrl, roomTemplatesUrl, submitting, error, info, turnstileEnabled, turnstileSiteKey } = props;
|
||||
|
||||
const [ step, setStep ] = useState<RegisterStep>('credentials');
|
||||
const [ email, setEmail ] = useState('');
|
||||
@@ -692,6 +698,10 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
|
||||
useEffect(() => { setLocalError(null); }, [ step ]);
|
||||
|
||||
const [ roomTemplates, setRoomTemplates ] = useState<RoomTemplate[] | null>(null);
|
||||
const [ roomTemplatesError, setRoomTemplatesError ] = useState<string | null>(null);
|
||||
const [ selectedTemplateId, setSelectedTemplateId ] = useState<number | null>(null);
|
||||
|
||||
const [ figureData, setFigureData ] = useState<FigureData | null>(null);
|
||||
const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
|
||||
const figureDataUrl = useMemo(() =>
|
||||
@@ -712,6 +722,31 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
return () => { cancelled = true; };
|
||||
}, [ step, figureData, figureDataUrl ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(step !== 'room' || roomTemplates !== null || !roomTemplatesUrl) return;
|
||||
let cancelled = false;
|
||||
setRoomTemplatesError(null);
|
||||
fetch(roomTemplatesUrl, { credentials: 'include' })
|
||||
.then(async r => {
|
||||
if(!r.ok) throw new Error(`status ${ r.status }`);
|
||||
return r.json();
|
||||
})
|
||||
.then(json => {
|
||||
if(cancelled) return;
|
||||
const list = Array.isArray((json as { templates?: unknown })?.templates)
|
||||
? (json as { templates: RoomTemplate[] }).templates
|
||||
: [];
|
||||
setRoomTemplates(list);
|
||||
})
|
||||
.catch(() => {
|
||||
if(cancelled) return;
|
||||
setRoomTemplates([]);
|
||||
setRoomTemplatesError('Could not load room options. You can still skip this step.');
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [ step, roomTemplates, roomTemplatesUrl ]);
|
||||
|
||||
const partOptions = useMemo(() =>
|
||||
{
|
||||
const result: Record<string, Record<GenderKey, number[]>> = {};
|
||||
@@ -974,22 +1009,35 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
setChecking(false);
|
||||
}
|
||||
|
||||
setStep('room');
|
||||
};
|
||||
|
||||
const submitRegistration = (templateId: number | null) =>
|
||||
{
|
||||
onSubmit({
|
||||
username: trimmed,
|
||||
username: username.trim(),
|
||||
email: email.trim(),
|
||||
password,
|
||||
figure,
|
||||
gender,
|
||||
turnstileToken
|
||||
turnstileToken,
|
||||
templateId
|
||||
}, resetWidget);
|
||||
};
|
||||
|
||||
const handleRoomSubmit = (event: FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
event.preventDefault();
|
||||
setLocalError(null);
|
||||
submitRegistration(selectedTemplateId);
|
||||
};
|
||||
|
||||
const busy = submitting || checking || pingingServer;
|
||||
const serverOffline = serverReachable === false;
|
||||
|
||||
return (
|
||||
<div className="nitro-login-modal">
|
||||
<div className={ `dialog ${ step === 'avatar' ? 'dialog-avatar' : '' }` }>
|
||||
<div className={ `dialog ${ step === 'avatar' ? 'dialog-avatar' : '' } ${ step === 'room' ? 'dialog-room' : '' }` }>
|
||||
<div className="nitro-login-card">
|
||||
<div className="card-title">
|
||||
<span>Habbo Details</span>
|
||||
@@ -1027,7 +1075,7 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
{ (localError || error) && <div className="error-line">{ localError || error }</div> }
|
||||
{ info && <div className="info-line">{ info }</div> }
|
||||
<div className="step-footer">
|
||||
<span className="step-indicator">1/2</span>
|
||||
<span className="step-indicator">1/3</span>
|
||||
<button type="submit" className="ok-button" disabled={ !credentialsValid || busy || serverOffline }>
|
||||
{ checking || pingingServer ? 'Checking…' : 'Next' }
|
||||
</button>
|
||||
@@ -1127,9 +1175,66 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
||||
|
||||
<div className="step-footer step-footer-split">
|
||||
<button type="button" className="ok-button back-button" onClick={ () => setStep('credentials') } disabled={ busy }>Back</button>
|
||||
<span className="step-indicator">2/2</span>
|
||||
<span className="step-indicator">2/3</span>
|
||||
<button type="submit" className="ok-button" disabled={ !username.trim() || busy || serverOffline }>
|
||||
{ submitting ? 'Creating…' : (checking || pingingServer) ? 'Checking…' : 'Next' }
|
||||
{ (checking || pingingServer) ? 'Checking…' : 'Next' }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
{ step === 'room' &&
|
||||
<form className="card-body" onSubmit={ handleRoomSubmit } autoComplete="off">
|
||||
<div className="register-intro">
|
||||
Last step — pick a starter room, or skip and create your own later.
|
||||
</div>
|
||||
{ serverOffline &&
|
||||
<div className="error-line server-offline">
|
||||
The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment.
|
||||
<button type="button" className="retry-link" onClick={ pingServer } disabled={ pingingServer }>
|
||||
{ pingingServer ? 'Checking…' : 'Retry' }
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="room-templates-list">
|
||||
<label className={ `room-template-option room-template-skip ${ selectedTemplateId === null ? 'selected' : '' }` }>
|
||||
<input type="radio" name="register-room-template" checked={ selectedTemplateId === null }
|
||||
onChange={ () => setSelectedTemplateId(null) } />
|
||||
<div className="room-template-body">
|
||||
<div className="room-template-title">I'm okay — I'll create my own rooms</div>
|
||||
<div className="room-template-description">Skip for now and start with an empty hotel inventory.</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{ roomTemplates === null && <div className="info-line">Loading rooms…</div> }
|
||||
|
||||
{ roomTemplates !== null && roomTemplates.map(template => (
|
||||
<label key={ template.templateId }
|
||||
className={ `room-template-option ${ selectedTemplateId === template.templateId ? 'selected' : '' }` }>
|
||||
<input type="radio" name="register-room-template" checked={ selectedTemplateId === template.templateId }
|
||||
onChange={ () => setSelectedTemplateId(template.templateId) } />
|
||||
{ template.thumbnail &&
|
||||
<img className="room-template-thumb" src={ template.thumbnail } alt={ template.title }
|
||||
onError={ e => { (e.currentTarget as HTMLImageElement).style.visibility = 'hidden'; } } /> }
|
||||
<div className="room-template-body">
|
||||
<div className="room-template-title">{ template.title }</div>
|
||||
{ template.description &&
|
||||
<div className="room-template-description">{ template.description }</div> }
|
||||
</div>
|
||||
</label>
|
||||
)) }
|
||||
</div>
|
||||
|
||||
{ roomTemplatesError && <div className="error-line">{ roomTemplatesError }</div> }
|
||||
{ (localError || error) && <div className="error-line">{ localError || error }</div> }
|
||||
{ info && <div className="info-line">{ info }</div> }
|
||||
|
||||
<div className="step-footer step-footer-split">
|
||||
<button type="button" className="ok-button back-button" onClick={ () => setStep('avatar') } disabled={ busy }>Back</button>
|
||||
<span className="step-indicator">3/3</span>
|
||||
<button type="submit" className="ok-button" disabled={ busy || serverOffline }>
|
||||
{ submitting ? 'Creating…' : 'Finish' }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -454,3 +454,70 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ─── Room template picker (step 3) ─── */
|
||||
|
||||
.nitro-login-card .room-templates-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #b6cfdd;
|
||||
border-radius: 4px;
|
||||
background: #eef4f8;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-option:hover {
|
||||
border-color: #7fa9c3;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-option.selected {
|
||||
border-color: #2e6b92;
|
||||
background: #d9e8f2;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-option input[type="radio"] {
|
||||
margin: 2px 0 0 0;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-thumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-title {
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
color: #0a2e45;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.nitro-login-card .room-template-description {
|
||||
font-size: 11px;
|
||||
color: #2a4a5c;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user