From 3f86946cdb7fd78d7effb0574b7750f4e5ef7ef0 Mon Sep 17 00:00:00 2001 From: Iveta Date: Tue, 19 Mar 2024 14:24:46 -0400 Subject: [PATCH] Accounts endpoint done --- .../explore-endpoints/[[...pages]]/page.tsx | 292 +++++++++----- .../(sidebar)/explore-endpoints/layout.tsx | 276 +------------ src/components/FormElements/AssetPicker.tsx | 284 ++++---------- src/components/FormElements/CursorPicker.tsx | 34 ++ src/components/FormElements/LimitPicker.tsx | 34 ++ src/components/FormElements/PubKeyPicker.tsx | 32 +- src/constants/exploreEndpointsPages.ts | 366 ++++++++++++++++++ src/constants/formComponentTemplate.tsx | 148 +++++++ src/helpers/parseJsonString.ts | 11 + src/helpers/sanitizeArray.ts | 3 + src/store/createStore.ts | 5 +- src/validate/methods/asset.ts | 44 +-- src/validate/methods/assetCode.ts | 4 +- 13 files changed, 882 insertions(+), 651 deletions(-) create mode 100644 src/components/FormElements/CursorPicker.tsx create mode 100644 src/components/FormElements/LimitPicker.tsx create mode 100644 src/constants/exploreEndpointsPages.ts create mode 100644 src/constants/formComponentTemplate.tsx create mode 100644 src/helpers/parseJsonString.ts create mode 100644 src/helpers/sanitizeArray.ts diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index 44121722..d160f7d1 100644 --- a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import { Alert, @@ -20,16 +20,29 @@ import { Routes } from "@/constants/routes"; import { WithInfoText } from "@/components/WithInfoText"; import { useStore } from "@/store/useStore"; -import { validate } from "@/validate"; import { isEmptyObject } from "@/helpers/isEmptyObject"; -import { AnyObject, Network } from "@/types/types"; +import { sanitizeArray } from "@/helpers/sanitizeArray"; +import { sanitizeObject } from "@/helpers/sanitizeObject"; +import { parseJsonString } from "@/helpers/parseJsonString"; -// TODO: build URL with valid params -// TODO: render fields based on route -// TODO: add streaming +import { EXPLORE_ENDPOINTS_PAGES_HORIZON } from "@/constants/exploreEndpointsPages"; +import { formComponentTemplate } from "@/constants/formComponentTemplate"; +import { AnyObject, AssetObject, Network } from "@/types/types"; + +// TODO: handle streaming export default function ExploreEndpoints() { const pathname = usePathname(); + const currentPage = pathname.split(Routes.EXPLORE_ENDPOINTS)?.[1]; + + const page = EXPLORE_ENDPOINTS_PAGES_HORIZON.navItems + .find((page) => pathname.includes(page.route)) + ?.nestedItems?.find((i) => i.route === pathname); + + const pageData = page?.form; + const requiredFields = sanitizeArray( + pageData?.requiredParams?.split(",") || [], + ); const { exploreEndpoints, network } = useStore(); const { @@ -42,28 +55,9 @@ export default function ExploreEndpoints() { resetParams, } = exploreEndpoints; - const requiredFields = ["sponsor"]; - - // TODO: fields to validate - const paramValidation = { - sponsor: validate.publicKey, - signer: validate.publicKey, - asset: validate.asset, - }; - - // TODO: - // const formParams = { - // sponsor: "", - // signer: "", - // // asset: "", - // cursor: "", - // limit: "", - // // order: "", - // }; - const [activeTab, setActiveTab] = useState("endpoints-tab-params"); const [formError, setFormError] = useState({}); - const currentPage = pathname.split(Routes.EXPLORE_ENDPOINTS)?.[1]; + const [requestUrl, setRequestUrl] = useState(""); const isSubmitEnabled = () => { const missingReqFields = requiredFields.reduce((res, cur) => { @@ -85,8 +79,8 @@ export default function ExploreEndpoints() { // Validate saved params when the page loads const paramErrors = () => { return Object.keys(params).reduce((res, param) => { - const error = (paramValidation as any)?.[param]( - params[param], + const error = formComponentTemplate(param)?.validate?.( + parseJsonString(params[param]), requiredFields.includes(param), ); @@ -129,9 +123,62 @@ export default function ExploreEndpoints() { } }, [endpointNetwork.id, network, resetParams, updateNetwork]); - if (pathname === Routes.EXPLORE_ENDPOINTS) { - return ; - } + const buildUrl = useCallback(() => { + const mapPathParamToValue = (pathParams: string[]) => { + return pathParams.map((pp) => params[pp] ?? pp).join("/"); + }; + + const endpointPath = `/accounts${pageData?.endpointPathParams ? `/${mapPathParamToValue(pageData.endpointPathParams.split(","))}` : ""}`; + const endpointParams = pageData?.endpointParams; + + const baseUrl = `${endpointNetwork.horizonUrl}${endpointPath}`; + const searchParams = new URLSearchParams(); + const templateParams = endpointParams?.split(","); + + const getParamRequestValue = (param: string) => { + const value = parseJsonString(params[param]); + + if (!value) { + return false; + } + + if (param === "asset") { + if (value.type === "native") { + return "native"; + } + + if (value.type === "none") { + return false; + } + + return `${value.code}:${value.issuer}`; + } + + return value; + }; + + // Build search params keeping the same params order + templateParams?.forEach((p) => { + const paramVal = getParamRequestValue(p); + + if (paramVal) { + searchParams.set(p, paramVal); + } + }); + + const searchParamString = searchParams.toString(); + + return `${baseUrl}${searchParamString ? `?${searchParamString}` : ""}`; + }, [ + endpointNetwork.horizonUrl, + pageData?.endpointParams, + pageData?.endpointPathParams, + params, + ]); + + useEffect(() => { + setRequestUrl(buildUrl()); + }, [buildUrl]); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); @@ -139,17 +186,23 @@ export default function ExploreEndpoints() { }; const renderEndpointUrl = () => { + if (!pageData) { + return null; + } + return (
GET
} + leftElement={ +
+ {pageData.requestMethod} +
+ } /> - {/* TODO: add text to copy */} - + @@ -168,81 +220,125 @@ export default function ExploreEndpoints() { }; const renderFields = () => { + if (!pageData) { + return null; + } + + const allFields = sanitizeArray([ + ...pageData.endpointPathParams.split(","), + ...pageData.endpointParams.split(","), + ]); + return (
- {/* TODO: render fields for path */} - {`Explore Endpoints: ${pathname}`} - -
- { - updateParams({ [e.target.id]: e.target.value }); - const error = paramValidation.sponsor( - e.target.value, - requiredFields.includes(e.target.id), - ); - - if (error) { - setFormError({ ...formError, [e.target.id]: error }); - } else { - if (formError[e.target.id]) { - const updatedErrors = { ...formError }; - delete updatedErrors[e.target.id]; - setFormError(updatedErrors); - } - } - }} - error={formError.sponsor} - /> + {allFields.map((f) => { + const component = formComponentTemplate(f, pageData.custom?.[f]); + + if (component) { + const isRequired = requiredFields.includes(f); + + switch (f) { + case "asset": + return component.render({ + value: params[f], + error: formError[f], + isRequired, + onChange: (assetObj: AssetObject) => { + updateParams({ + [f]: isEmptyObject(sanitizeObject(assetObj || {})) + ? undefined + : JSON.stringify(assetObj), + }); + const error = component.validate?.(assetObj, isRequired); + + if (error) { + setFormError({ ...formError, [f]: error }); + } else { + if (formError[f]) { + const updatedErrors = { ...formError }; + delete updatedErrors[f]; + setFormError(updatedErrors); + } + } + }, + }); + case "order": + return component.render({ + value: params[f], + error: formError[f], + isRequired, + onChange: (optionId: string | undefined) => { + updateParams({ [f]: optionId }); + const error = component.validate?.(optionId, isRequired); + + if (error) { + setFormError({ ...formError, [f]: error }); + } else { + if (formError[f]) { + const updatedErrors = { ...formError }; + delete updatedErrors[f]; + setFormError(updatedErrors); + } + } + }, + }); + default: + return component.render({ + value: params[f], + error: formError[f], + isRequired, + onChange: (e: React.ChangeEvent) => { + updateParams({ [f]: e.target.value }); + const error = component.validate?.( + e.target.value, + isRequired, + ); + + if (error) { + setFormError({ ...formError, [f]: error }); + } else { + if (formError[f]) { + const updatedErrors = { ...formError }; + delete updatedErrors[f]; + setFormError(updatedErrors); + } + } + }, + }); + } + } + + return null; + })} +
- + { - updateParams({ [e.target.id]: e.target.value }); - const error = paramValidation.signer( - e.target.value, - requiredFields.includes(e.target.id), - ); - - if (error) { - setFormError({ ...formError, [e.target.id]: error }); - } else { - if (formError[e.target.id]) { - const updatedErrors = { ...formError }; - delete updatedErrors[e.target.id]; - setFormError(updatedErrors); - } - } - }} - error={formError.signer} /> -
-
- - - - + + ) : null} ); }; + if (pathname === Routes.EXPLORE_ENDPOINTS) { + return ; + } + + if (!pageData) { + return <>{`${page?.label} page is coming soon.`}; + } + return ( <>
+ {children} ); } - -const horizon_endpoints = { - instruction: "Horizon Endpoints", - navItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_ACCOUNTS, - label: "Accounts", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_ACCOUNTS, - label: "All Accounts", - }, - { - route: Routes.EXPLORE_ENDPOINTS_ACCOUNTS_SINGLE, - label: "Single Account", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_ASSETS, - label: "Assets", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_ASSETS, - label: "All Assets", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_CLAIMABLE_BALANCES, - label: "Claimable Balances", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_CLAIMABLE_BALANCES, - label: "All Claimable Balances", - }, - { - route: Routes.EXPLORE_ENDPOINTS_CLAIMABLE_BALANCES_SINGLE, - label: "Single Claimable Balance", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS, - label: "Effects", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS, - label: "All Effects", - }, - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS_ACCOUNT, - label: "Effects for Account", - }, - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS_LEDGER, - label: "Effects for Ledger", - }, - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS_LIQUIDITY_POOL, - label: "Effects for Liquidity Pool", - }, - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS_OPERATION, - label: "Effects for Operation", - }, - { - route: Routes.EXPLORE_ENDPOINTS_EFFECTS_TRANSACTION, - label: "Effects for Transaction", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_FEE_STATS, - label: "Fee Stats", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_FEE_STATS, - label: "All Fee Stats", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_LEDGERS, - label: "Ledgers", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_LEDGERS, - label: "All Ledgers", - }, - { - route: Routes.EXPLORE_ENDPOINTS_LEDGERS_SINGLE, - label: "Single Ledger", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_LIQUIDITY_POOLS, - label: "Liquidity Pools", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_LIQUIDITY_POOLS, - label: "All Liquidity Pools", - }, - { - route: Routes.EXPLORE_ENDPOINTS_LIQUIDITY_POOLS_SINGLE, - label: "Single Liquidity Pool", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_OFFERS, - label: "Offers", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_OFFERS, - label: "All Offers", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OFFERS_SINGLE, - label: "Single Offer", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OFFERS_ACCOUNT, - label: "Offers for Account", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS, - label: "Operations", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS, - label: "All Operations", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_SINGLE, - label: "Single Operation", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_ACCOUNT, - label: "Operations for Account", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_LEDGER, - label: "Operations for Ledger", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_LIQUIDITY_POOL, - label: "Operations for Liquidity Pool", - }, - { - route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_TRANSACTION, - label: "Operations for Transaction", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_ORDER_BOOK_DETAILS, - label: "Order Book", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_ORDER_BOOK_DETAILS, - label: "Details", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_PATHS_PAYMENT, - label: "Paths", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_PATHS_PAYMENT, - label: "Find Payment Paths", - }, - { - route: Routes.EXPLORE_ENDPOINTS_PATHS_STRICT_RECEIVE, - label: "Find Strict Receive Payment Paths", - }, - { - route: Routes.EXPLORE_ENDPOINTS_PATHS_STRICT_SEND, - label: "Find Strict Send Payment Paths", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_PAYMENTS, - label: "Payments", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_PAYMENTS, - label: "All Payments", - }, - { - route: Routes.EXPLORE_ENDPOINTS_PAYMENTS_ACCOUNT, - label: "Payments for Account", - }, - { - route: Routes.EXPLORE_ENDPOINTS_PAYMENTS_LEDGER, - label: "Payments for Ledger", - }, - { - route: Routes.EXPLORE_ENDPOINTS_PAYMENTS_TRANSACTION, - label: "Payments for Transaction", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRADE_AGGREGATIONS, - label: "Trade Aggregations", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_TRADE_AGGREGATIONS, - label: "All Trade Aggregations", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRADES, - label: "Trades", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_TRADES, - label: "All Trades", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRADES_ACCOUNT, - label: "Trades for Account", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRADES_LIQUIDITY_POOL, - label: "Trades for Liquidity Pool", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRADES_OFFER, - label: "Trades for Offer", - }, - ], - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS, - label: "Transactions", - nestedItems: [ - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS, - label: "All Transactions", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_SINGLE, - label: "Single Transaction", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_POST, - label: "Post Transaction", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_ACCOUNT, - label: "Transactions for Account", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_LEDGER, - label: "Transactions for Ledger", - }, - { - route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_LIQUIDITY_POOL, - label: "Transactions for Liquidity Pool", - }, - ], - }, - ], -}; diff --git a/src/components/FormElements/AssetPicker.tsx b/src/components/FormElements/AssetPicker.tsx index 5ceda7b9..716db1b3 100644 --- a/src/components/FormElements/AssetPicker.tsx +++ b/src/components/FormElements/AssetPicker.tsx @@ -1,267 +1,143 @@ -import { useState } from "react"; import { Input } from "@stellar/design-system"; import { ExpandBox } from "@/components/ExpandBox"; import { RadioPicker } from "@/components/RadioPicker"; import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; -import { - AssetObject, - AssetObjectValue, - AssetString, - AssetType, -} from "@/types/types"; +import { AssetObject, AssetObjectValue } from "@/types/types"; -type AssetPickerProps = ( - | { - variant: "string"; - value: string | undefined; - includeNone?: boolean; - includeNative?: undefined; - onChange: ( - optionId: AssetType | undefined, - optionValue: string | undefined, - ) => void; - } - | { - variant: "object"; - value: AssetObjectValue | undefined; - includeNone?: undefined; - includeNative?: boolean; - onChange: ( - optionId: AssetType | undefined, - optionValue: AssetObjectValue | undefined, - ) => void; - } -) & { +type AssetPickerProps = { id: string; - selectedOption: AssetType | undefined; label: string; labelSuffix?: string | React.ReactNode; + value: AssetObjectValue | undefined; + error: { code: string | undefined; issuer: string | undefined } | undefined; + onChange: (asset: AssetObjectValue | undefined) => void; + assetInput: "issued" | "alphanumeric"; fitContent?: boolean; + includeNone?: boolean; + includeNative?: boolean; }; export const AssetPicker = ({ id, - variant, - selectedOption, label, - value, - includeNone, - includeNative = true, - onChange, labelSuffix, + value = { type: undefined, code: "", issuer: "" }, + error, + onChange, + assetInput, fitContent, + includeNone, + includeNative = true, }: AssetPickerProps) => { - const initErrorState = { - code: "", - issuer: "", - }; - - const getInitialValue = () => { - if (variant === "string") { - const assetString = value?.split(":"); - return { - type: undefined, - code: assetString?.[0] ?? "", - issuer: assetString?.[1] ?? "", - }; - } + let options: AssetObject[] = []; - return { - type: value?.type, - code: value?.code ?? "", - issuer: value?.issuer ?? "", - }; - }; - - const [stateValue, setStateValue] = useState(getInitialValue()); - const [error, setError] = useState(initErrorState); - - let stringOptions: AssetString[] = [ - { - id: "native", - label: "Native", - value: "native", - }, - { - id: "issued", - label: "Issued", - value: "", - }, - ]; + if (includeNative) { + options = [ + { + id: "native", + label: "Native", + value: { + type: "native", + code: "", + issuer: "", + }, + }, + ...options, + ]; + } if (includeNone) { - stringOptions = [ + options = [ { id: "none", label: "None", - value: "", + value: { + type: "none", + code: "", + issuer: "", + }, }, - ...stringOptions, + ...options, ]; } - let objectOptions: AssetObject[] = [ - { - id: "credit_alphanum4", - label: "Alphanumeric 4", - value: { - type: "credit_alphanum4", - code: "", - issuer: "", + if (assetInput === "alphanumeric") { + options = [ + ...options, + { + id: "credit_alphanum4", + label: "Alphanumeric 4", + value: { + type: "credit_alphanum4", + code: "", + issuer: "", + }, }, - }, - { - id: "credit_alphanum12", - label: "Alphanumeric 12", - value: { - type: "credit_alphanum12", - code: "", - issuer: "", + { + id: "credit_alphanum12", + label: "Alphanumeric 12", + value: { + type: "credit_alphanum12", + code: "", + issuer: "", + }, }, - }, - // TODO: add Liquidity Pool shares (for Change Trust operation) - ]; - - if (includeNative) { - objectOptions = [ + // TODO: add Liquidity Pool shares (for Change Trust operation) + ]; + } else { + options = [ + ...options, { - id: "native", - label: "Native", + id: "issued", + label: "Issued", value: { - type: "native", + type: "issued", code: "", issuer: "", }, }, - ...objectOptions, ]; } - // Extra helper function to make TypeScript happy to get the right type - const handleOnChange = ( - id: AssetType | undefined, - value: string | AssetObjectValue | undefined, - ) => { - if (!value) { - onChange(id, undefined); - } - - if (variant === "string") { - onChange(id, value as string); - } else { - onChange(id, value as AssetObjectValue); - } - }; - - const handleOptionChange = ( - optionId: AssetType | undefined, - optionValue: string | AssetObjectValue | undefined, - ) => { - handleOnChange(optionId, optionValue); - setStateValue({ type: optionId, code: "", issuer: "" }); - setError(initErrorState); - }; - - const validateCode = (code: string, assetType: AssetType | undefined) => { - if (!code) { - return "Asset code is required."; - } - - let minLength; - let maxLength; - - switch (assetType) { - case "credit_alphanum4": - minLength = 1; - maxLength = 4; - break; - case "credit_alphanum12": - minLength = 5; - maxLength = 12; - break; - default: - minLength = 1; - maxLength = 12; - } - - if (!code.match(/^[a-zA-Z0-9]+$/g)) { - return "Asset code must consist of only letters and numbers."; - } else if (code.length < minLength || code.length > maxLength) { - return `Asset code must be between ${minLength} and ${maxLength} characters long.`; - } - - return undefined; - }; - - const handleCodeError = (value: string) => { - const codeError = validateCode(value, stateValue.type); - setError({ ...error, code: codeError || "" }); - return codeError; - }; - return (
{ + onChange({ type: optionId, code: "", issuer: "" }); + }} + options={options} fitContent={fitContent} /> ) => { - setStateValue({ ...stateValue, code: e.target.value }); - handleCodeError(e.target.value); - }, - onBlur: (e) => { - const codeError = handleCodeError(e.target.value); - - if (!codeError && stateValue.issuer) { - handleOnChange( - selectedOption, - variant === "string" - ? `${stateValue.code}:${stateValue.issuer}` - : stateValue, - ); - } + onChange({ ...value, code: e.target.value }); }, - error: error.code, + error: error?.code || "", }} issuer={{ - value: stateValue.issuer, - onChange: (value: string, issuerError: string) => { - setStateValue({ ...stateValue, issuer: value }); - setError({ ...error, issuer: issuerError }); - }, - onBlur: (value, issuerError) => { - setError({ ...error, issuer: issuerError }); - - if (!issuerError && stateValue.code) { - handleOnChange( - selectedOption, - variant === "string" - ? `${stateValue.code}:${value}` - : stateValue, - ); - } + value: value.issuer, + onChange: (e: React.ChangeEvent) => { + onChange({ ...value, issuer: e.target.value }); }, - error: error.issuer, + error: error?.issuer || "", }} /> @@ -275,13 +151,11 @@ type AssetPickerFieldsProps = { value: string; error: string; onChange: (e: React.ChangeEvent) => void; - onBlur: (e: React.ChangeEvent) => void; }; issuer: { value: string; error: string; - onChange: (value: string, issuerError: string) => void; - onBlur: (value: string, issuerError: string) => void; + onChange: (e: React.ChangeEvent) => void; }; }; @@ -293,7 +167,6 @@ const AssetPickerFields = ({ id, code, issuer }: AssetPickerFieldsProps) => ( label="Asset Code" value={code.value} onChange={code.onChange} - onBlur={code.onBlur} error={code.error} /> ( placeholder="Example: GCEXAMPLE5HWNK4AYSTEQ4UWDKHTCKADVS2AHF3UI2ZMO3DPUSM6Q4UG" value={issuer.value} onChange={issuer.onChange} - onBlur={issuer.onBlur} error={issuer.error} />
diff --git a/src/components/FormElements/CursorPicker.tsx b/src/components/FormElements/CursorPicker.tsx new file mode 100644 index 00000000..09b12aa5 --- /dev/null +++ b/src/components/FormElements/CursorPicker.tsx @@ -0,0 +1,34 @@ +import { Input, InputProps } from "@stellar/design-system"; + +interface CursorPickerProps extends Omit { + id: string; + fieldSize?: "sm" | "md" | "lg"; + labelSuffix?: string | React.ReactNode; + placeholder?: string; + value: string; + error: string | undefined; + onChange: (e: React.ChangeEvent) => void; +} + +export const CursorPicker = ({ + id, + fieldSize = "md", + labelSuffix, + value, + error, + onChange, + ...props +}: CursorPickerProps) => { + return ( + + ); +}; diff --git a/src/components/FormElements/LimitPicker.tsx b/src/components/FormElements/LimitPicker.tsx new file mode 100644 index 00000000..2af361cf --- /dev/null +++ b/src/components/FormElements/LimitPicker.tsx @@ -0,0 +1,34 @@ +import { Input, InputProps } from "@stellar/design-system"; + +interface LimitPickerProps extends Omit { + id: string; + fieldSize?: "sm" | "md" | "lg"; + labelSuffix?: string | React.ReactNode; + placeholder?: string; + value: string; + error: string | undefined; + onChange: (e: React.ChangeEvent) => void; +} + +export const LimitPicker = ({ + id, + fieldSize = "md", + labelSuffix, + value, + error, + onChange, + ...props +}: LimitPickerProps) => { + return ( + + ); +}; diff --git a/src/components/FormElements/PubKeyPicker.tsx b/src/components/FormElements/PubKeyPicker.tsx index 53dd4a58..45857f58 100644 --- a/src/components/FormElements/PubKeyPicker.tsx +++ b/src/components/FormElements/PubKeyPicker.tsx @@ -1,4 +1,3 @@ -import { StrKey } from "stellar-sdk"; import { Input, InputProps } from "@stellar/design-system"; interface PubKeyPickerProps extends Omit { @@ -8,9 +7,8 @@ interface PubKeyPickerProps extends Omit { labelSuffix?: string | React.ReactNode; placeholder?: string; value: string; - error: string | ""; - onChange: (value: string, error: string) => void; - onBlur: (value: string, error: string) => void; + error: string | undefined; + onChange: (e: React.ChangeEvent) => void; } export const PubKeyPicker = ({ @@ -22,25 +20,8 @@ export const PubKeyPicker = ({ value, error, onChange, - onBlur, ...props }: PubKeyPickerProps) => { - const validatePublicKey = (issuer: string) => { - if (!issuer) { - return "Asset issuer is required."; - } - - if (issuer.startsWith("M")) { - if (!StrKey.isValidMed25519PublicKey(issuer)) { - return "Muxed account address is invalid."; - } - } else if (!StrKey.isValidEd25519PublicKey(issuer)) { - return "Public key is invalid."; - } - - return ""; - }; - return ( { - const error = validatePublicKey(e.target.value); - onChange(e.target.value, error); - }} - onBlur={(e) => { - const error = validatePublicKey(e.target.value); - onBlur(e.target.value, error); - }} error={error} + onChange={onChange} {...props} /> ); diff --git a/src/constants/exploreEndpointsPages.ts b/src/constants/exploreEndpointsPages.ts new file mode 100644 index 00000000..ac3564da --- /dev/null +++ b/src/constants/exploreEndpointsPages.ts @@ -0,0 +1,366 @@ +import { Routes } from "@/constants/routes"; +import { AnyObject } from "@/types/types"; + +type ExploreEndpointsPagesProps = { + instruction: string; + navItems: { + route: Routes; + label: string; + nestedItems: { + route: Routes; + label: string; + form: + | { + info: string; + requestMethod: "GET" | "POST"; + endpointPath: string; + endpointPathParams: string; + endpointParams: string; + requiredParams: string; + isStreaming?: boolean; + custom?: AnyObject; + } + // TODO: remove once all pages are filled + | undefined; + }[]; + }[]; +}; + +export const EXPLORE_ENDPOINTS_PAGES_HORIZON: ExploreEndpointsPagesProps = { + instruction: "Horizon Endpoints", + navItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_ACCOUNTS, + label: "Accounts", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_ACCOUNTS, + label: "All Accounts", + form: { + info: "https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/accounts", + requestMethod: "GET", + endpointPath: "/accounts", + endpointPathParams: "", + endpointParams: "sponsor,signer,asset,cursor,limit,order", + requiredParams: "", + isStreaming: true, + custom: { + asset: { + assetInput: "issued", + includeNone: true, + includeNative: true, + }, + }, + }, + }, + { + route: Routes.EXPLORE_ENDPOINTS_ACCOUNTS_SINGLE, + label: "Single Account", + form: { + info: "https://developers.stellar.org/api/resources/accounts/single/", + requestMethod: "GET", + endpointPath: "/accounts", + endpointPathParams: "account_id", + endpointParams: "", + requiredParams: "account_id", + isStreaming: false, + }, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_ASSETS, + label: "Assets", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_ASSETS, + label: "All Assets", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_CLAIMABLE_BALANCES, + label: "Claimable Balances", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_CLAIMABLE_BALANCES, + label: "All Claimable Balances", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_CLAIMABLE_BALANCES_SINGLE, + label: "Single Claimable Balance", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS, + label: "Effects", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS, + label: "All Effects", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS_ACCOUNT, + label: "Effects for Account", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS_LEDGER, + label: "Effects for Ledger", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS_LIQUIDITY_POOL, + label: "Effects for Liquidity Pool", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS_OPERATION, + label: "Effects for Operation", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_EFFECTS_TRANSACTION, + label: "Effects for Transaction", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_FEE_STATS, + label: "Fee Stats", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_FEE_STATS, + label: "All Fee Stats", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_LEDGERS, + label: "Ledgers", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_LEDGERS, + label: "All Ledgers", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_LEDGERS_SINGLE, + label: "Single Ledger", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_LIQUIDITY_POOLS, + label: "Liquidity Pools", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_LIQUIDITY_POOLS, + label: "All Liquidity Pools", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_LIQUIDITY_POOLS_SINGLE, + label: "Single Liquidity Pool", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_OFFERS, + label: "Offers", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_OFFERS, + label: "All Offers", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OFFERS_SINGLE, + label: "Single Offer", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OFFERS_ACCOUNT, + label: "Offers for Account", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS, + label: "Operations", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS, + label: "All Operations", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_SINGLE, + label: "Single Operation", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_ACCOUNT, + label: "Operations for Account", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_LEDGER, + label: "Operations for Ledger", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_LIQUIDITY_POOL, + label: "Operations for Liquidity Pool", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_OPERATIONS_TRANSACTION, + label: "Operations for Transaction", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_ORDER_BOOK_DETAILS, + label: "Order Book", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_ORDER_BOOK_DETAILS, + label: "Details", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_PATHS_PAYMENT, + label: "Paths", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_PATHS_PAYMENT, + label: "Find Payment Paths", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_PATHS_STRICT_RECEIVE, + label: "Find Strict Receive Payment Paths", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_PATHS_STRICT_SEND, + label: "Find Strict Send Payment Paths", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_PAYMENTS, + label: "Payments", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_PAYMENTS, + label: "All Payments", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_PAYMENTS_ACCOUNT, + label: "Payments for Account", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_PAYMENTS_LEDGER, + label: "Payments for Ledger", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_PAYMENTS_TRANSACTION, + label: "Payments for Transaction", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRADE_AGGREGATIONS, + label: "Trade Aggregations", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_TRADE_AGGREGATIONS, + label: "All Trade Aggregations", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRADES, + label: "Trades", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_TRADES, + label: "All Trades", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRADES_ACCOUNT, + label: "Trades for Account", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRADES_LIQUIDITY_POOL, + label: "Trades for Liquidity Pool", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRADES_OFFER, + label: "Trades for Offer", + form: undefined, + }, + ], + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS, + label: "Transactions", + nestedItems: [ + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS, + label: "All Transactions", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_SINGLE, + label: "Single Transaction", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_POST, + label: "Post Transaction", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_ACCOUNT, + label: "Transactions for Account", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_LEDGER, + label: "Transactions for Ledger", + form: undefined, + }, + { + route: Routes.EXPLORE_ENDPOINTS_TRANSACTIONS_LIQUIDITY_POOL, + label: "Transactions for Liquidity Pool", + form: undefined, + }, + ], + }, + ], +}; diff --git a/src/constants/formComponentTemplate.tsx b/src/constants/formComponentTemplate.tsx new file mode 100644 index 00000000..5b791600 --- /dev/null +++ b/src/constants/formComponentTemplate.tsx @@ -0,0 +1,148 @@ +import { AssetPicker } from "@/components/FormElements/AssetPicker"; +import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; + +import { parseJsonString } from "@/helpers/parseJsonString"; +import { validate } from "@/validate"; +import { AnyObject, AssetObjectValue } from "@/types/types"; +import { OrderPicker } from "@/components/FormElements/OrderPicker"; +import { CursorPicker } from "@/components/FormElements/CursorPicker"; +import { LimitPicker } from "@/components/FormElements/LimitPicker"; + +type TemplateRenderProps = { + value: string | undefined; + error: string | undefined; + onChange: (val: any) => void; + isRequired?: boolean; +}; + +type TemplateRenderAssetProps = { + value: AssetObjectValue | undefined; + error: { code: string | undefined; issuer: string | undefined } | undefined; + onChange: (asset: AssetObjectValue | undefined) => void; + isRequired?: boolean; +}; + +type TemplateRenderOrderProps = { + value: string | undefined; + onChange: (optionId: string | undefined, optionValue?: string) => void; + isRequired?: boolean; +}; + +type FormComponentTemplate = { + render: (...args: any[]) => JSX.Element; + validate: ((...args: any[]) => any) | null; +}; + +export const formComponentTemplate = ( + id: string, + custom?: AnyObject, +): FormComponentTemplate | null => { + switch (id) { + case "account_id": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.publicKey, + }; + case "asset": + return { + render: (templ: TemplateRenderAssetProps) => ( + + ), + validate: validate.asset, + }; + case "cursor": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: null, + }; + case "limit": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.positiveInt, + }; + case "order": + return { + render: (templ: TemplateRenderOrderProps) => ( + + ), + validate: null, + }; + case "signer": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.publicKey, + }; + case "sponsor": + return { + render: (templ: TemplateRenderProps) => ( + + ), + validate: validate.publicKey, + }; + default: + return null; + } +}; diff --git a/src/helpers/parseJsonString.ts b/src/helpers/parseJsonString.ts new file mode 100644 index 00000000..ea7ff6f4 --- /dev/null +++ b/src/helpers/parseJsonString.ts @@ -0,0 +1,11 @@ +export const parseJsonString = (value: any | undefined) => { + if (value) { + try { + return JSON.parse(value) as T; + } catch (e) { + return value; + } + } + + return value; +}; diff --git a/src/helpers/sanitizeArray.ts b/src/helpers/sanitizeArray.ts new file mode 100644 index 00000000..05d90e1a --- /dev/null +++ b/src/helpers/sanitizeArray.ts @@ -0,0 +1,3 @@ +export const sanitizeArray = (array: any[]) => { + return array.filter((i) => Boolean(i)); +}; diff --git a/src/store/createStore.ts b/src/store/createStore.ts index 1e963395..e2147db1 100644 --- a/src/store/createStore.ts +++ b/src/store/createStore.ts @@ -2,8 +2,8 @@ import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; import { querystring } from "zustand-querystring"; -import { AnyObject, EmptyObj, Network } from "@/types/types"; import { sanitizeObject } from "@/helpers/sanitizeObject"; +import { AnyObject, EmptyObj, Network } from "@/types/types"; export interface Store { // Shared @@ -28,12 +28,9 @@ export interface Store { // Explore Endpoints exploreEndpoints: { - // TODO: do we need this? network: Network | EmptyObj; currentEndpoint: string | undefined; - // TODO: ??? type every endpoint and use that type here params: AnyObject; - // TODO: move to params? isStreaming: boolean; updateNetwork: (network: Network) => void; updateCurrentEndpoint: (endpoint: string) => void; diff --git a/src/validate/methods/asset.ts b/src/validate/methods/asset.ts index c82f6518..89ce7892 100644 --- a/src/validate/methods/asset.ts +++ b/src/validate/methods/asset.ts @@ -1,51 +1,19 @@ import { isEmptyObject } from "@/helpers/isEmptyObject"; import { assetCode } from "./assetCode"; import { publicKey } from "./publicKey"; - -// TODO: remove once other PR is merged -type AssetType = - | "none" - | "native" - | "issued" - | "credit_alphanum4" - | "credit_alphanum12" - | "liquidity_pool_shares"; - -type AssetObjectValue = { - type: AssetType | undefined; - code: string; - issuer: string; -}; +import { AssetObjectValue } from "@/types/types"; export const asset = ( - asset: string | AssetObjectValue, + asset: AssetObjectValue | undefined, isRequired?: boolean, ) => { - let code; - let issuer; - let type; - - if (typeof asset === "string") { - // No need to validate native asset - if (asset === "native") { - return false; - } - - [code, issuer] = asset.split(":"); - } else { - // No need to validate native asset - if (asset.type === "native") { - return false; - } - - code = asset?.code; - issuer = asset?.issuer; - type = asset?.type; + if (asset?.type && ["none", "native"].includes(asset.type)) { + return false; } const invalid = Object.entries({ - code: assetCode(code, type, isRequired), - issuer: publicKey(issuer, isRequired), + code: assetCode(asset?.code || "", asset?.type, isRequired), + issuer: publicKey(asset?.issuer || "", isRequired), }).reduce((res, cur) => { const [key, value] = cur; diff --git a/src/validate/methods/assetCode.ts b/src/validate/methods/assetCode.ts index aee5c54b..cac83797 100644 --- a/src/validate/methods/assetCode.ts +++ b/src/validate/methods/assetCode.ts @@ -5,8 +5,8 @@ export const assetCode = ( assetType: AssetType | undefined, isRequired?: boolean, ) => { - if (isRequired && !code) { - return "Asset code is required."; + if (!code) { + return isRequired ? "Asset code is required." : false; } let minLength;