mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +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 right = interpolate(loginImages['right'] || GetConfigurationValue<string>('login_right', ''));
|
||||||
const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
|
const loginUrl = GetConfigurationValue<string>('login.endpoint', '/api/auth/login');
|
||||||
const registerUrl = GetConfigurationValue<string>('login.register.endpoint', '/api/auth/register');
|
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 forgotUrl = GetConfigurationValue<string>('login.forgot.endpoint', '/api/auth/forgot-password');
|
||||||
const turnstileSiteKey = GetConfigurationValue<string>('login.turnstile.sitekey', '');
|
const turnstileSiteKey = GetConfigurationValue<string>('login.turnstile.sitekey', '');
|
||||||
const rawTurnstileEnabled = GetConfigurationValue<unknown>('login.turnstile.enabled', false);
|
const rawTurnstileEnabled = GetConfigurationValue<unknown>('login.turnstile.enabled', false);
|
||||||
@@ -316,7 +317,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
}
|
}
|
||||||
}, [ checkUsernameUrl, postJson ]);
|
}, [ 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)
|
if(turnstileEnabled && !body.turnstileToken)
|
||||||
{
|
{
|
||||||
@@ -336,6 +337,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
password: body.password,
|
password: body.password,
|
||||||
figure: body.figure,
|
figure: body.figure,
|
||||||
gender: body.gender,
|
gender: body.gender,
|
||||||
|
templateId: body.templateId ?? undefined,
|
||||||
turnstileToken: turnstileEnabled ? body.turnstileToken : undefined
|
turnstileToken: turnstileEnabled ? body.turnstileToken : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -491,6 +493,7 @@ export const LoginView: FC<LoginViewProps> = ({ onAuthenticated }) =>
|
|||||||
onCheckUsername={ checkUsernameAvailable }
|
onCheckUsername={ checkUsernameAvailable }
|
||||||
onCheckServer={ checkServerReachable }
|
onCheckServer={ checkServerReachable }
|
||||||
imagingUrl={ imagingUrl }
|
imagingUrl={ imagingUrl }
|
||||||
|
roomTemplatesUrl={ roomTemplatesUrl }
|
||||||
submitting={ submitting }
|
submitting={ submitting }
|
||||||
error={ error }
|
error={ error }
|
||||||
info={ info }
|
info={ info }
|
||||||
@@ -524,14 +527,17 @@ interface DialogSharedProps
|
|||||||
|
|
||||||
interface RegisterDialogProps extends 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 }>;
|
onCheckEmail: (email: string) => Promise<{ available: boolean; error?: string }>;
|
||||||
onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>;
|
onCheckUsername: (username: string) => Promise<{ available: boolean; error?: string }>;
|
||||||
onCheckServer: () => Promise<boolean>;
|
onCheckServer: () => Promise<boolean>;
|
||||||
imagingUrl: string;
|
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@]+$/;
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
@@ -642,7 +648,7 @@ const buildPartPreviewUrl = (
|
|||||||
|
|
||||||
const RegisterDialog: FC<RegisterDialogProps> = props =>
|
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 [ step, setStep ] = useState<RegisterStep>('credentials');
|
||||||
const [ email, setEmail ] = useState('');
|
const [ email, setEmail ] = useState('');
|
||||||
@@ -692,6 +698,10 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
|
|
||||||
useEffect(() => { setLocalError(null); }, [ step ]);
|
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 [ figureData, setFigureData ] = useState<FigureData | null>(null);
|
||||||
const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
|
const figureDataUrlRaw = GetConfigurationValue<string>('avatar.figuredata.url', '');
|
||||||
const figureDataUrl = useMemo(() =>
|
const figureDataUrl = useMemo(() =>
|
||||||
@@ -712,6 +722,31 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [ step, figureData, figureDataUrl ]);
|
}, [ 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 partOptions = useMemo(() =>
|
||||||
{
|
{
|
||||||
const result: Record<string, Record<GenderKey, number[]>> = {};
|
const result: Record<string, Record<GenderKey, number[]>> = {};
|
||||||
@@ -974,22 +1009,35 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
setChecking(false);
|
setChecking(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStep('room');
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitRegistration = (templateId: number | null) =>
|
||||||
|
{
|
||||||
onSubmit({
|
onSubmit({
|
||||||
username: trimmed,
|
username: username.trim(),
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
password,
|
password,
|
||||||
figure,
|
figure,
|
||||||
gender,
|
gender,
|
||||||
turnstileToken
|
turnstileToken,
|
||||||
|
templateId
|
||||||
}, resetWidget);
|
}, resetWidget);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRoomSubmit = (event: FormEvent<HTMLFormElement>) =>
|
||||||
|
{
|
||||||
|
event.preventDefault();
|
||||||
|
setLocalError(null);
|
||||||
|
submitRegistration(selectedTemplateId);
|
||||||
|
};
|
||||||
|
|
||||||
const busy = submitting || checking || pingingServer;
|
const busy = submitting || checking || pingingServer;
|
||||||
const serverOffline = serverReachable === false;
|
const serverOffline = serverReachable === false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="nitro-login-modal">
|
<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="nitro-login-card">
|
||||||
<div className="card-title">
|
<div className="card-title">
|
||||||
<span>Habbo Details</span>
|
<span>Habbo Details</span>
|
||||||
@@ -1027,7 +1075,7 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
{ (localError || error) && <div className="error-line">{ localError || error }</div> }
|
{ (localError || error) && <div className="error-line">{ localError || error }</div> }
|
||||||
{ info && <div className="info-line">{ info }</div> }
|
{ info && <div className="info-line">{ info }</div> }
|
||||||
<div className="step-footer">
|
<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 }>
|
<button type="submit" className="ok-button" disabled={ !credentialsValid || busy || serverOffline }>
|
||||||
{ checking || pingingServer ? 'Checking…' : 'Next' }
|
{ checking || pingingServer ? 'Checking…' : 'Next' }
|
||||||
</button>
|
</button>
|
||||||
@@ -1127,9 +1175,66 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
|
|||||||
|
|
||||||
<div className="step-footer step-footer-split">
|
<div className="step-footer step-footer-split">
|
||||||
<button type="button" className="ok-button back-button" onClick={ () => setStep('credentials') } disabled={ busy }>Back</button>
|
<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 }>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -454,3 +454,70 @@
|
|||||||
font-size: 11px;
|
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