refactor: the login's code step and the reservation row shapes get modules of their own

Both files had grown past their size limit with this round's fixes. The
two-factor step moves to pages/login/MfaCodeStep.tsx and the reservation row
types to reservation-rows.ts, re-exported from the repository, and both size
baselines come down to match.
This commit is contained in:
Maurice
2026-11-10 09:55:36 +01:00
parent 0ac06555cb
commit 6b086eaed5
6 changed files with 177 additions and 146 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
"src/index.css": 1636,
"src/mobile/screens/admin/MAdminPluginsPanel.tsx": 1506,
"src/pages/JourneyDetailPage.test.tsx": 4028,
"src/pages/LoginPage.tsx": 1347,
"src/pages/LoginPage.tsx": 1294,
"src/pages/TripPlannerPage.tsx": 1128,
"src/pages/atlas/useAtlas.ts": 1207,
"src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3518,
+13 -88
View File
@@ -4,7 +4,6 @@ import {
EyeOff,
Fingerprint,
Globe,
KeyRound,
Loader2,
Lock,
Mail,
@@ -18,6 +17,7 @@ import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n';
import { IDP_LOGIN_URL, useLogin } from './login/useLogin';
import LoginWorld from './login/LoginWorld';
import NativeServerLink from './login/NativeServerLink';
import MfaCodeStep from './login/MfaCodeStep';
import PasswordChecklist from '../components/shared/PasswordChecklist';
/** Fixed so the sky does not reshuffle on every render. */
@@ -739,93 +739,18 @@ export default function LoginPage(): React.ReactElement {
)}
{mode === 'login' && mfaStep && !passwordChangeStep && (
<div>
<label
style={{
display: 'block',
fontSize: 'calc(12.5px * var(--fs-scale-body, 1))',
fontWeight: 600,
color: '#374151',
marginBottom: 6,
}}
>
{t('login.mfaCodeLabel')}
</label>
{/* The account the code belongs to, for password managers: they match a
one-time code to a saved login through the username in the same form,
and the email field is gone in this step. */}
<input
type="text"
name="username"
autoComplete="username"
value={email}
readOnly
tabIndex={-1}
aria-hidden="true"
className="sr-only"
/>
<div style={{ position: 'relative' }}>
<KeyRound
size={15}
className="text-[#9ca3af]"
style={{
position: 'absolute',
insetInlineStart: 13,
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
}}
/>
<input
type="text"
id="totp"
name="totp"
// Text, not numeric: backup codes carry letters. The hints are set here
// rather than left to native/keyboardFields.ts, because autoFocus gives
// the field focus before any touch could prepare it.
inputMode="text"
autoComplete="one-time-code"
autoCapitalize="characters"
autoCorrect="off"
spellCheck={false}
enterKeyHint="go"
value={mfaCode}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMfaCode(e.target.value.toUpperCase().slice(0, 24))
}
placeholder={t('login.mfaPlaceholder')}
required
autoFocus
style={inputBase}
onFocus={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#111827')}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#e5e7eb')}
/>
</div>
<p style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: '#9ca3af', marginTop: 8 }}>
{t('login.mfaHint')}
</p>
<button
type="button"
onClick={() => {
setMfaStep(false);
setMfaToken('');
setMfaCode('');
setError('');
}}
style={{
marginTop: 8,
background: 'none',
border: 'none',
color: '#6b7280',
fontSize: 'calc(13px * var(--fs-scale-body, 1))',
cursor: 'pointer',
padding: 0,
fontFamily: 'inherit',
}}
>
{t('login.mfaBack')}
</button>
</div>
<MfaCodeStep
email={email}
code={mfaCode}
onCode={setMfaCode}
inputStyle={inputBase}
onBack={() => {
setMfaStep(false);
setMfaToken('');
setMfaCode('');
setError('');
}}
/>
)}
{/* Username (register only) */}
+99
View File
@@ -0,0 +1,99 @@
import { KeyRound } from 'lucide-react';
import React from 'react';
import { useTranslation } from '../../i18n';
interface MfaCodeStepProps {
/** The account being signed in, kept in the form for password managers. */
email: string;
code: string;
onCode: (code: string) => void;
inputStyle: React.CSSProperties;
onBack: () => void;
}
/** The login's second step: the one-time or backup code, and the way back. */
export default function MfaCodeStep({ email, code, onCode, inputStyle, onBack }: MfaCodeStepProps) {
const { t } = useTranslation();
return (
<div>
<label
style={{
display: 'block',
fontSize: 'calc(12.5px * var(--fs-scale-body, 1))',
fontWeight: 600,
color: '#374151',
marginBottom: 6,
}}
>
{t('login.mfaCodeLabel')}
</label>
{/* The account the code belongs to, for password managers: they match a
one-time code to a saved login through the username in the same form,
and the email field is gone in this step. */}
<input
type="text"
name="username"
autoComplete="username"
value={email}
readOnly
tabIndex={-1}
aria-hidden="true"
className="sr-only"
/>
<div style={{ position: 'relative' }}>
<KeyRound
size={15}
className="text-[#9ca3af]"
style={{
position: 'absolute',
insetInlineStart: 13,
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
}}
/>
<input
type="text"
id="totp"
name="totp"
// Text, not numeric: backup codes carry letters. The hints are set here
// rather than left to native/keyboardFields.ts, because autoFocus gives
// the field focus before any touch could prepare it.
inputMode="text"
autoComplete="one-time-code"
autoCapitalize="characters"
autoCorrect="off"
spellCheck={false}
enterKeyHint="go"
value={code}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onCode(e.target.value.toUpperCase().slice(0, 24))}
placeholder={t('login.mfaPlaceholder')}
required
autoFocus
style={inputStyle}
onFocus={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#111827')}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => (e.target.style.borderColor = '#e5e7eb')}
/>
</div>
<p style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: '#9ca3af', marginTop: 8 }}>
{t('login.mfaHint')}
</p>
<button
type="button"
onClick={onBack}
style={{
marginTop: 8,
background: 'none',
border: 'none',
color: '#6b7280',
fontSize: 'calc(13px * var(--fs-scale-body, 1))',
cursor: 'pointer',
padding: 0,
fontFamily: 'inherit',
}}
>
{t('login.mfaBack')}
</button>
</div>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@
"scripts/db-call-graph.mjs": 1036,
"scripts/generate-entities.ts": 2259,
"src/db/repositories/Places.repository.ts": 1437,
"src/db/repositories/Reservations.repository.ts": 1604,
"src/db/repositories/Reservations.repository.ts": 1559,
"src/db/repositories/Trips.repository.ts": 1121,
"src/db/repositories/Users.repository.ts": 1445,
"src/nest/atlas/atlas.service.ts": 1177,
@@ -18,7 +18,10 @@ import type {
PublicApiUnplannedPlaceRow,
PublicApiUnscheduledReservationRow,
} from './reservation-public-api-rows';
import type { ReservationAllColumnsRow, ReservationJoinRow } from './reservation-rows';
// The reservation row shapes live in their own module; re-exported so callers keep one import path.
export type { ReservationAllColumnsRow, ReservationJoinRow } from './reservation-rows';
// The public API row types moved to their own module; re-exported so callers keep one import path.
export type {
PublicApiAccommodationRow,
@@ -27,31 +30,6 @@ export type {
PublicApiUnscheduledReservationRow,
};
/**
* RS18/RR1's shared joined projection — `SELECT r.*, d.day_number, p.name as
* place_name, r.assignment_id, ap.place_id as accommodation_place_id,
* acc_p.name as accommodation_name, ap.start_day_id as
* accommodation_start_day_id, ap.end_day_id as accommodation_end_day_id,
* ap.check_in/check_in_end/check_out as accommodation_check_in/_check_in_end/_check_out FROM
* reservations r LEFT JOIN days d ON r.day_id = d.id LEFT JOIN places p ON
* r.place_id = p.id LEFT JOIN day_accommodations ap ON r.accommodation_id =
* ap.id LEFT JOIN places acc_p ON ap.place_id = acc_p.id`. The service
* folds `accommodation_id` (`Math.trunc(Number(x))`), `day_positions`,
* `endpoints` and `travelers` on afterward (unchanged from the legacy split).
*/
export interface ReservationJoinRow extends ReservationAllColumnsRow {
day_number: number | null;
place_name: string | null;
accommodation_place_id: number | null;
accommodation_name: string | null;
accommodation_start_day_id: number | null;
accommodation_end_day_id: number | null;
/** The stay's own times, which a hotel booking's card falls back to. */
accommodation_check_in: string | null;
accommodation_check_in_end: string | null;
accommodation_check_out: string | null;
}
/**
* Kysely typing for the RS18/RR1 join (`joinedQuery` below) — `reservations
* .accommodation_id` is a bare `p.text().nullable()` column with no FK to
@@ -1534,34 +1512,3 @@ export interface RoadtripCarrierRow {
metadata: string | null;
day_plan_position: number | null;
}
/** `SELECT *` — every scalar column of `Reservations`, RS35's shape. */
export interface ReservationAllColumnsRow {
id: number;
trip_id: number;
day_id: number | null;
end_day_id: number | null;
place_id: number | null;
assignment_id: number | null;
title: string;
accommodation_id: string | null;
reservation_time: string | null;
reservation_end_time: string | null;
location: string | null;
confirmation_number: string | null;
notes: string | null;
status: string | null;
type: string | null;
created_at: string | null;
metadata: string | null;
day_plan_position: number | null;
needs_review: number;
external_source: string | null;
external_id: string | null;
external_owner_user_id: number | null;
external_synced_at: string | null;
sync_enabled: number | null;
external_hash: string | null;
url: string | null;
ingest_state: string;
}
@@ -0,0 +1,60 @@
/**
* The row shapes ReservationsRepository reads: every column of a reservation,
* and the joined projection the trip list and the single reads share.
*/
/** `SELECT *` — every scalar column of `Reservations`, RS35's shape. */
export interface ReservationAllColumnsRow {
id: number;
trip_id: number;
day_id: number | null;
end_day_id: number | null;
place_id: number | null;
assignment_id: number | null;
title: string;
accommodation_id: string | null;
reservation_time: string | null;
reservation_end_time: string | null;
location: string | null;
confirmation_number: string | null;
notes: string | null;
status: string | null;
type: string | null;
created_at: string | null;
metadata: string | null;
day_plan_position: number | null;
needs_review: number;
external_source: string | null;
external_id: string | null;
external_owner_user_id: number | null;
external_synced_at: string | null;
sync_enabled: number | null;
external_hash: string | null;
url: string | null;
ingest_state: string;
}
/**
* RS18/RR1's shared joined projection — `SELECT r.*, d.day_number, p.name as
* place_name, r.assignment_id, ap.place_id as accommodation_place_id,
* acc_p.name as accommodation_name, ap.start_day_id as
* accommodation_start_day_id, ap.end_day_id as accommodation_end_day_id,
* ap.check_in/check_in_end/check_out as accommodation_check_in/_check_in_end/_check_out FROM
* reservations r LEFT JOIN days d ON r.day_id = d.id LEFT JOIN places p ON
* r.place_id = p.id LEFT JOIN day_accommodations ap ON r.accommodation_id =
* ap.id LEFT JOIN places acc_p ON ap.place_id = acc_p.id`. The service
* folds `accommodation_id` (`Math.trunc(Number(x))`), `day_positions`,
* `endpoints` and `travelers` on afterward (unchanged from the legacy split).
*/
export interface ReservationJoinRow extends ReservationAllColumnsRow {
day_number: number | null;
place_name: string | null;
accommodation_place_id: number | null;
accommodation_name: string | null;
accommodation_start_day_id: number | null;
accommodation_end_day_id: number | null;
/** The stay's own times, which a hotel booking's card falls back to. */
accommodation_check_in: string | null;
accommodation_check_in_end: string | null;
accommodation_check_out: string | null;
}