diff --git a/src/app/pairs/new/page.tsx b/src/app/pairs/new/page.tsx index 9589cb3..3223be7 100644 --- a/src/app/pairs/new/page.tsx +++ b/src/app/pairs/new/page.tsx @@ -1,133 +1,142 @@ -"use client"; - -import { type FormEvent, useState } from "react"; -import { useRouter } from "next/navigation"; -import { TextField } from "@/components/TextField"; -import { apiPost } from "@/lib/apiClient"; -import { assetsDiffer } from "@/lib/quote"; - -const ASSET_CODE_RE = /^[A-Za-z0-9]{1,12}$/; -const ASSET_CODE_ERROR = "Use 1-12 ASCII letters or numbers."; - -type FormErrors = { - source?: string; - destination?: string; - form?: string; -}; - -/** - * Trims and normalizes Stellar asset codes after validating the raw code - * characters are ASCII alphanumeric only. - */ -function normalizeAssetCode(value: string): string | null { - const trimmed = value.trim(); - return ASSET_CODE_RE.test(trimmed) ? trimmed.toUpperCase() : null; -} - -export default function NewPairPage() { - const router = useRouter(); - const [source, setSource] = useState(""); - const [destination, setDestination] = useState(""); - const [errors, setErrors] = useState({}); - const [loading, setLoading] = useState(false); - - const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - const normalizedSource = normalizeAssetCode(source); - const normalizedDestination = normalizeAssetCode(destination); - const nextErrors: FormErrors = {}; - - if (!normalizedSource) { - nextErrors.source = ASSET_CODE_ERROR; - } - if (!normalizedDestination) { - nextErrors.destination = ASSET_CODE_ERROR; - } - if ( - normalizedSource && - normalizedDestination && - normalizedSource === normalizedDestination - ) { - nextErrors.destination = "Source and destination must differ."; - } - - if (Object.keys(nextErrors).length > 0 || !normalizedSource || !normalizedDestination) { - setErrors(nextErrors); - return; - } - - setErrors({}); - setSource(normalizedSource); - setDestination(normalizedDestination); - setLoading(true); - try { - await apiPost("/api/v1/pairs", { - source: normalizedSource, - destination: normalizedDestination, - }); - router.push("/pairs"); - } catch (err) { - setErrors({ form: (err as Error).message }); - } finally { - setLoading(false); - } - }; - - return ( -
-

New pair

-
- { - setSource(e.target.value); - setErrors((current) => ({ - ...current, - source: undefined, - destination: - current.destination === "Source and destination must differ." - ? undefined - : current.destination, - form: undefined, - })); - }} - error={errors.source} - /> - { - setDestination(e.target.value); - setErrors((current) => ({ - ...current, - destination: undefined, - form: undefined, - })); - }} - error={errors.destination} - /> - - {errors.form && ( -

- {errors.form} -

- )} - -
- ); -} +"use client"; + +import { type FormEvent, useState } from "react"; +import { useRouter } from "next/navigation"; +import { TextField } from "@/components/TextField"; +import { apiPost } from "@/lib/apiClient"; +import { assetsDiffer } from "@/lib/quote"; + +const ASSET_CODE_RE = /^[A-Za-z0-9]{1,12}$/; +const ASSET_CODE_ERROR = "Use 1-12 ASCII letters or numbers."; + +type FormErrors = { + source?: string; + destination?: string; + form?: string; +}; + +/** + * Trims and normalizes Stellar asset codes after validating the raw code + * characters are ASCII alphanumeric only. + */ +function normalizeAssetCode(value: string): string | null { + const trimmed = value.trim(); + return ASSET_CODE_RE.test(trimmed) ? trimmed.toUpperCase() : null; +} + +export default function NewPairPage() { + const router = useRouter(); + const [source, setSource] = useState(""); + const [destination, setDestination] = useState(""); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + const [statusNote, setStatusNote] = useState(null); + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + const normalizedSource = normalizeAssetCode(source); + const normalizedDestination = normalizeAssetCode(destination); + const nextErrors: FormErrors = {}; + + if (!normalizedSource) { + nextErrors.source = ASSET_CODE_ERROR; + } + if (!normalizedDestination) { + nextErrors.destination = ASSET_CODE_ERROR; + } + if ( + normalizedSource && + normalizedDestination && + normalizedSource === normalizedDestination + ) { + nextErrors.destination = "Source and destination must differ."; + } + + if (Object.keys(nextErrors).length > 0 || !normalizedSource || !normalizedDestination) { + setErrors(nextErrors); + return; + } + + setErrors({}); + setSource(normalizedSource); + setDestination(normalizedDestination); + setLoading(true); + setStatusNote("Registering pair…"); + try { + await apiPost("/api/v1/pairs", { + source: normalizedSource, + destination: normalizedDestination, + }); + setStatusNote("Pair registered. Redirecting…"); + router.push("/pairs"); + } catch (err) { + setErrors({ form: (err as Error).message }); + setStatusNote(null); + } finally { + setLoading(false); + } + }; + + return ( +
+

New pair

+
+ { + setSource(e.target.value); + setErrors((current) => ({ + ...current, + source: undefined, + destination: + current.destination === "Source and destination must differ." + ? undefined + : current.destination, + form: undefined, + })); + }} + error={errors.source} + aria-invalid={errors.source ? true : undefined} + /> + { + setDestination(e.target.value); + setErrors((current) => ({ + ...current, + destination: undefined, + form: undefined, + })); + }} + error={errors.destination} + aria-invalid={errors.destination ? true : undefined} + /> + +

+ {statusNote ?? ""} +

+ {errors.form && ( +

+ {errors.form} +

+ )} + +
+ ); +}