diff --git a/backend/app/controllers/api/auth.py b/backend/app/controllers/api/auth.py index 962d9d8..581b9d9 100644 --- a/backend/app/controllers/api/auth.py +++ b/backend/app/controllers/api/auth.py @@ -116,7 +116,9 @@ def register() -> Any: with IdentityUnitOfWork(customer_id=None, role=SessionRole.ADMIN) as uow: existing = uow.customers.get_by_email(data.email) if existing is not None: - raise ValidationError("Email already registered") + # Distinct code (not the shared `validation_failed`) so the client can offer a real + # next step -- "log in instead" -- rather than the generic "check your details". + raise ValidationError("Email already registered", code="email_taken") customer = Customer(email=data.email, password_hash=hash_password(data.password)) uow.customers.add(customer) diff --git a/backend/tests/api/test_auth.py b/backend/tests/api/test_auth.py index 0face53..46df9a2 100644 --- a/backend/tests/api/test_auth.py +++ b/backend/tests/api/test_auth.py @@ -87,7 +87,9 @@ def test_register_rejects_a_duplicate_email(api_client: FlaskClient) -> None: second = _register(api_client) assert second.status_code == 422 - assert second.get_json()["code"] == "validation_failed" + # Distinct code -- not the shared `validation_failed` -- so the client can offer "log in + # instead" rather than a generic "check your details" message. + assert second.get_json()["code"] == "email_taken" @pytest.mark.parametrize( diff --git a/frontend/src/components/Icon/paths.js b/frontend/src/components/Icon/paths.js index 50327f9..7721dc3 100644 --- a/frontend/src/components/Icon/paths.js +++ b/frontend/src/components/Icon/paths.js @@ -27,4 +27,13 @@ export const ICON_PATHS = { 'M3.8 13v-2.8h2.8', ], menu: ['M2.5 4.5h11', 'M2.5 8h11', 'M2.5 11.5h11'], + eye: [ + 'M2 8c1.6-3.2 4-4.8 6-4.8s4.4 1.6 6 4.8c-1.6 3.2-4 4.8-6 4.8s-4.4-1.6-6-4.8z', + 'M8 8m-1.8 0a1.8 1.8 0 1 0 3.6 0a1.8 1.8 0 1 0 -3.6 0', + ], + 'eye-off': [ + 'M2 8c1.6-3.2 4-4.8 6-4.8s4.4 1.6 6 4.8c-1.6 3.2-4 4.8-6 4.8s-4.4-1.6-6-4.8z', + 'M8 8m-1.8 0a1.8 1.8 0 1 0 3.6 0a1.8 1.8 0 1 0 -3.6 0', + 'M3 3 13 13', + ], } diff --git a/frontend/src/components/Input/Input.css b/frontend/src/components/Input/Input.css index b305f3a..d5a3a56 100644 --- a/frontend/src/components/Input/Input.css +++ b/frontend/src/components/Input/Input.css @@ -4,6 +4,13 @@ gap: var(--space-1); } +.tu-field__label-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); +} + .tu-field__label { font-size: var(--font-size-body); font-weight: var(--font-weight-body-medium); diff --git a/frontend/src/components/Input/Input.jsx b/frontend/src/components/Input/Input.jsx index 63bb06f..c9fd85b 100644 --- a/frontend/src/components/Input/Input.jsx +++ b/frontend/src/components/Input/Input.jsx @@ -1,17 +1,24 @@ import { useId } from 'react' import './Input.css' -/** Design system §7.2. Label always visible above the field — never placeholder-as-label. */ -export function Input({ label, id, error, hint, className = '', ...rest }) { +/** + * Design system §7.2. Label always visible above the field — never placeholder-as-label. + * `labelExtra` is an optional node rendered at the label row's trailing edge (e.g. a + * show/hide-password toggle) — additive and unused by every existing caller. + */ +export function Input({ label, id, error, hint, labelExtra, className = '', ...rest }) { const generatedId = useId() const fieldId = id || generatedId const messageId = error || hint ? `${fieldId}-message` : undefined return (