From ce54d7bc53202f29ea2b9b2e1f521721e0159d54 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 22 Apr 2026 16:26:49 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Added=20Step=203=20for=20UI=20lo?= =?UTF-8?q?gin=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/login/LoginView.tsx | 125 ++++++++++++++++++++++++++--- src/css/login/LoginView.css | 67 ++++++++++++++++ 2 files changed, 182 insertions(+), 10 deletions(-) diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index 7ab8457..024b56e 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -66,6 +66,7 @@ export const LoginView: FC = ({ onAuthenticated }) => const right = interpolate(loginImages['right'] || GetConfigurationValue('login_right', '')); const loginUrl = GetConfigurationValue('login.endpoint', '/api/auth/login'); const registerUrl = GetConfigurationValue('login.register.endpoint', '/api/auth/register'); + const roomTemplatesUrl = GetConfigurationValue('login.room_templates.endpoint', '/api/auth/room-templates'); const forgotUrl = GetConfigurationValue('login.forgot.endpoint', '/api/auth/forgot-password'); const turnstileSiteKey = GetConfigurationValue('login.turnstile.sitekey', ''); const rawTurnstileEnabled = GetConfigurationValue('login.turnstile.enabled', false); @@ -316,7 +317,7 @@ export const LoginView: FC = ({ 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 = ({ 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 = ({ 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; 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 = 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('credentials'); const [ email, setEmail ] = useState(''); @@ -692,6 +698,10 @@ const RegisterDialog: FC = props => useEffect(() => { setLocalError(null); }, [ step ]); + const [ roomTemplates, setRoomTemplates ] = useState(null); + const [ roomTemplatesError, setRoomTemplatesError ] = useState(null); + const [ selectedTemplateId, setSelectedTemplateId ] = useState(null); + const [ figureData, setFigureData ] = useState(null); const figureDataUrlRaw = GetConfigurationValue('avatar.figuredata.url', ''); const figureDataUrl = useMemo(() => @@ -712,6 +722,31 @@ const RegisterDialog: FC = 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> = {}; @@ -974,22 +1009,35 @@ const RegisterDialog: FC = 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) => + { + event.preventDefault(); + setLocalError(null); + submitRegistration(selectedTemplateId); + }; + const busy = submitting || checking || pingingServer; const serverOffline = serverReachable === false; return (
-
+
Habbo Details @@ -1027,7 +1075,7 @@ const RegisterDialog: FC = props => { (localError || error) &&
{ localError || error }
} { info &&
{ info }
}
- 1/2 + 1/3 @@ -1127,9 +1175,66 @@ const RegisterDialog: FC = props =>
- 2/2 + 2/3 +
+ + } + + { step === 'room' && +
+
+ Last step — pick a starter room, or skip and create your own later. +
+ { serverOffline && +
+ The gameserver isn't running right now, so new accounts can't be created. Please try again in a moment. + +
+ } + +
+ + + { roomTemplates === null &&
Loading rooms…
} + + { roomTemplates !== null && roomTemplates.map(template => ( + + )) } +
+ + { roomTemplatesError &&
{ roomTemplatesError }
} + { (localError || error) &&
{ localError || error }
} + { info &&
{ info }
} + +
+ + 3/3 +
diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index 984a68f..7798934 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -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; +} +