import { useState } from 'react'; import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { Card, TextField, Button, Callout, Badge, Dialog, IconButton, } from '@olly/modern-sk'; import { Icon } from '../../components/common/Icon'; import { useAppDispatch } from '../../hooks/useAppDispatch'; import { useConnectionStatus } from '../../hooks/useConnectionStatus'; import { setTokens, setUser } from '../../store/slices/auth'; import { useLoginMutation, useRegisterMutation, } from '../../api/endpoints/auth'; import { REGISTRATION_ENABLED } from '../../config/env'; import { listInstances, getActiveInstanceId, setActiveInstanceId, removeInstance, clearInstanceAuth, upsertInstance, type Instance, } from '../../config/instances'; type Mode = 'login' | 'register'; const HEALTH_VARIANTS = { connected: 'lime', connecting: 'neutral', disconnected: 'ember', error: 'ember', } as const; const HEALTH_KEY = { connected: 'conn.connected', connecting: 'conn.connecting', disconnected: 'conn.disconnected', error: 'conn.error', } as const; /** Map an RTKQ login failure to a user-facing i18n key. */ function resolveLoginError(err: unknown): string { const e = err as FetchBaseQueryError | undefined; if (e && 'status' in e) { if (e.status === 'FETCH_ERROR') return 'connect.errors.unreachable'; if (e.status === 401) return 'connect.errors.badCredentials'; } return 'connect.errors.generic'; } /** Map an RTKQ register failure to a user-facing i18n key. */ function resolveRegisterError(err: unknown): string { const e = err as FetchBaseQueryError | undefined; if (e && 'status' in e) { if (e.status === 'FETCH_ERROR') return 'connect.errors.unreachable'; if (e.status === 409) return 'connect.errors.usernameTaken'; if (e.status === 422) return 'connect.errors.passwordTooShort'; if (e.status === 403) return 'connect.errors.registrationDisabled'; } return 'connect.errors.registerFailed'; } function InstanceRow({ inst, selected, onSelect, onLogout, onRemove, }: { inst: Instance; selected: boolean; onSelect: () => void; onLogout: () => void; onRemove: () => void; }) { const { t } = useTranslation(); const status = useConnectionStatus(inst.baseUrl); const [dialogOpen, setDialogOpen] = useState(false); return (
{t(HEALTH_KEY[status])}
{inst.name}
{inst.baseUrl}
{selected ? ( {t('connect.domains.selected')} ) : ( )} } footer={
} />
); } export function ConnectPage() { const { t } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const [rev, setRev] = useState(0); const instances = listInstances(); const [selectedId, setSelectedId] = useState(() => getActiveInstanceId(), ); const selectedInstance = instances.find((i) => i.id === selectedId) ?? null; const [instanceAddShown, setInstanceAddShown] = useState(false); const [addUrl, setAddUrl] = useState(''); const [mode, setMode] = useState('login'); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(null); const [login, { isLoading: isLoggingIn }] = useLoginMutation(); const [register, { isLoading: isRegistering }] = useRegisterMutation(); const isLoading = isLoggingIn || isRegistering; const switchMode = (next: Mode) => { setMode(next); setError(null); }; // Switching the active instance and reloading lets the app pick the saved // session for that instance back up (if any); if it has none, ProtectedRoute // bounces back here and `selectedId` defaults to it, surfacing the login card. const selectInstance = (id: string) => { setActiveInstanceId(id); window.location.assign('/'); }; const handleAdd = (e: React.FormEvent) => { e.preventDefault(); const url = addUrl.trim(); if (!url || url === 'https://') return; const inst = upsertInstance(url); setActiveInstanceId(inst.id); setAddUrl('https://'); setSelectedId(inst.id); setRev((r) => r + 1); }; const handleLogout = (id: string) => { clearInstanceAuth(id); setRev((r) => r + 1); }; const handleRemove = (id: string) => { removeInstance(id); if (selectedId === id) setSelectedId(getActiveInstanceId()); setRev((r) => r + 1); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedInstance) return; setError(null); try { const action = mode === 'register' ? register({ username, password }) : login({ username, password }); const { user, tokens } = await action.unwrap(); dispatch(setTokens(tokens)); dispatch(setUser(user)); void navigate('/'); } catch (err) { setError( mode === 'register' ? resolveRegisterError(err) : resolveLoginError(err), ); } }; const labelStyle: React.CSSProperties = { display: 'block', fontSize: '0.8125rem', fontWeight: 500, marginBottom: '0.375rem', color: 'var(--color-text-2)', }; return (

MCMA

{instances.length > 0 && ( <> {t('connect.domains.title')} {instances.map((inst) => ( selectInstance(inst.id)} onLogout={() => handleLogout(inst.id)} onRemove={() => handleRemove(inst.id)} /> ))} )}
0 ? '0.5rem' : 0, }} > {instanceAddShown ? ( <> setAddUrl(e.target.value)} placeholder={t('connect.domains.addPlaceholder')} style={{ flex: 1 }} /> ) : ( )}
{selectedInstance && (
{mode === 'register' ? t('connect.login.registerTitle', { name: selectedInstance.name, }) : t('connect.login.title', { name: selectedInstance.name })}
setUsername(e.target.value)} placeholder="username" autoComplete="username" required />
setPassword(e.target.value)} placeholder="password" autoComplete={ mode === 'register' ? 'new-password' : 'current-password' } required /> {mode === 'register' && ( {t('connect.login.passwordHint')} )}
{error && {t(error)}} {REGISTRATION_ENABLED && (
{mode === 'register' ? ( <> {t('connect.login.haveAccount')}{' '} ) : ( <> {t('connect.login.noAccount')}{' '} )}
)}
)}
); }