From 7ae0e66778d3843f72a9de725986924fbd787c8a Mon Sep 17 00:00:00 2001 From: Iveta Date: Fri, 15 Mar 2024 10:50:37 -0400 Subject: [PATCH 1/8] Explore Endpoints: data flow functional --- .../explore-endpoints/[[...pages]]/page.tsx | 167 +++++++++++++++++- src/helpers/isEmptyObject.ts | 5 + src/helpers/sanitizeObject.ts | 13 ++ src/hooks/usePrevious.ts | 11 ++ src/store/createStore.ts | 74 +++++++- src/types/types.ts | 1 + src/validate/index.ts | 11 ++ src/validate/methods/asset.ts | 60 +++++++ src/validate/methods/assetCode.ts | 36 ++++ src/validate/methods/positiveInt.ts | 9 + src/validate/methods/publicKey.ts | 21 +++ 11 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 src/helpers/isEmptyObject.ts create mode 100644 src/helpers/sanitizeObject.ts create mode 100644 src/hooks/usePrevious.ts create mode 100644 src/validate/index.ts create mode 100644 src/validate/methods/asset.ts create mode 100644 src/validate/methods/assetCode.ts create mode 100644 src/validate/methods/positiveInt.ts create mode 100644 src/validate/methods/publicKey.ts diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index 33b49d27..44121722 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 { useState } from "react"; +import { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import { Alert, @@ -19,10 +19,115 @@ import { SdsLink } from "@/components/SdsLink"; 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"; + +// TODO: build URL with valid params +// TODO: render fields based on route +// TODO: add streaming + export default function ExploreEndpoints() { const pathname = usePathname(); + const { exploreEndpoints, network } = useStore(); + const { + params, + currentEndpoint, + network: endpointNetwork, + updateParams, + updateCurrentEndpoint, + updateNetwork, + 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 isSubmitEnabled = () => { + const missingReqFields = requiredFields.reduce((res, cur) => { + if (!params[cur]) { + return [...res, cur]; + } + + return res; + }, [] as string[]); + + if (missingReqFields.length !== 0) { + return false; + } + + return isEmptyObject(formError); + }; + + useEffect(() => { + // Validate saved params when the page loads + const paramErrors = () => { + return Object.keys(params).reduce((res, param) => { + const error = (paramValidation as any)?.[param]( + params[param], + requiredFields.includes(param), + ); + + if (error) { + return { ...res, [param]: error }; + } + + return res; + }, {}); + }; + + setFormError(paramErrors()); + + // We want to check this only when the page mounts for the first time + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (currentPage) { + updateCurrentEndpoint(currentPage); + } + + // Clear form and errors if navigating to another endpoint page. We don't + // want to keep previous form values. + if (currentEndpoint && currentEndpoint !== currentPage) { + resetParams(); + setFormError({}); + } + }, [currentPage, currentEndpoint, updateCurrentEndpoint, resetParams]); + + useEffect(() => { + // Save network for endpoints if we don't have it yet. + if (network.id && !endpointNetwork.id) { + updateNetwork(network as Network); + // When network changes, clear saved params and errors. + } else if (network.id && network.id !== endpointNetwork.id) { + resetParams(); + setFormError({}); + updateNetwork(network as Network); + } + }, [endpointNetwork.id, network, resetParams, updateNetwork]); if (pathname === Routes.EXPLORE_ENDPOINTS) { return ; @@ -46,8 +151,12 @@ export default function ExploreEndpoints() { // TODO: set request type leftElement={
GET
} /> - {/* TODO: disable if can't submit */} - {/* TODO: add text to copy */} @@ -64,6 +173,58 @@ export default function ExploreEndpoints() {
{/* 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} + /> + + { + 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} + /> +
diff --git a/src/helpers/isEmptyObject.ts b/src/helpers/isEmptyObject.ts new file mode 100644 index 00000000..3547627e --- /dev/null +++ b/src/helpers/isEmptyObject.ts @@ -0,0 +1,5 @@ +import { AnyObject } from "@/types/types"; + +export const isEmptyObject = (obj: AnyObject) => { + return Object.keys(obj).length === 0; +}; diff --git a/src/helpers/sanitizeObject.ts b/src/helpers/sanitizeObject.ts new file mode 100644 index 00000000..54268caa --- /dev/null +++ b/src/helpers/sanitizeObject.ts @@ -0,0 +1,13 @@ +import { AnyObject } from "@/types/types"; + +export const sanitizeObject = (obj: T) => { + return Object.keys(obj).reduce((res, param) => { + const paramValue = obj[param]; + + if (paramValue) { + return { ...res, [param]: paramValue }; + } + + return res; + }, {} as T); +}; diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts new file mode 100644 index 00000000..ea66eb11 --- /dev/null +++ b/src/hooks/usePrevious.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from "react"; + +export const usePrevious = (value: T) => { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +}; diff --git a/src/store/createStore.ts b/src/store/createStore.ts index a47d98e6..1e963395 100644 --- a/src/store/createStore.ts +++ b/src/store/createStore.ts @@ -1,12 +1,15 @@ import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; import { querystring } from "zustand-querystring"; -import { EmptyObj, Network } from "@/types/types"; + +import { AnyObject, EmptyObj, Network } from "@/types/types"; +import { sanitizeObject } from "@/helpers/sanitizeObject"; export interface Store { // Shared network: Network | EmptyObj; selectNetwork: (network: Network) => void; + resetStoredData: () => void; // Account account: { @@ -22,22 +25,57 @@ export interface Store { }) => void; reset: () => void; }; + + // 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; + updateParams: (params: AnyObject) => void; + resetParams: () => void; + reset: () => void; + }; } interface CreateStoreOptions { url?: string; } +// Initial states +const initExploreEndpointState = { + network: {}, + currentEndpoint: undefined, + params: {}, + isStreaming: false, +}; + // Store export const createStore = (options: CreateStoreOptions) => create()( + // https://github.com/nitedani/zustand-querystring querystring( immer((set) => ({ + // Shared network: {}, selectNetwork: (network: Network) => set((state) => { state.network = network; }), + resetStoredData: () => + set((state) => { + // Add stores that need global reset + state.exploreEndpoints = { + ...state.exploreEndpoints, + ...initExploreEndpointState, + }; + }), + // Account account: { value: "", nestedObject: { @@ -60,6 +98,36 @@ export const createStore = (options: CreateStoreOptions) => state.account.value = ""; }), }, + // Explore Endpoints + exploreEndpoints: { + ...initExploreEndpointState, + updateNetwork: (network: Network) => + set((state) => { + state.exploreEndpoints.network = network; + }), + updateCurrentEndpoint: (endpoint: string) => + set((state) => { + state.exploreEndpoints.currentEndpoint = endpoint; + }), + updateParams: (params: AnyObject) => + set((state) => { + state.exploreEndpoints.params = sanitizeObject({ + ...state.exploreEndpoints.params, + ...params, + }); + }), + resetParams: () => + set((state) => { + state.exploreEndpoints.params = {}; + }), + reset: () => + set((state) => { + state.exploreEndpoints = { + ...state.exploreEndpoints, + ...initExploreEndpointState, + }; + }), + }, })), { url: options.url, @@ -68,6 +136,10 @@ export const createStore = (options: CreateStoreOptions) => return { network: true, account: true, + exploreEndpoints: { + params: true, + isStreaming: true, + }, }; }, key: "||", diff --git a/src/types/types.ts b/src/types/types.ts index d621088f..a4177afc 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,6 +1,7 @@ // ============================================================================= // Generic // ============================================================================= +export type AnyObject = { [key: string]: any }; export type EmptyObj = Record; // ============================================================================= diff --git a/src/validate/index.ts b/src/validate/index.ts new file mode 100644 index 00000000..45e11173 --- /dev/null +++ b/src/validate/index.ts @@ -0,0 +1,11 @@ +import { asset } from "./methods/asset"; +import { assetCode } from "./methods/assetCode"; +import { positiveInt } from "./methods/positiveInt"; +import { publicKey } from "./methods/publicKey"; + +export const validate = { + asset, + assetCode, + positiveInt, + publicKey, +}; diff --git a/src/validate/methods/asset.ts b/src/validate/methods/asset.ts new file mode 100644 index 00000000..c82f6518 --- /dev/null +++ b/src/validate/methods/asset.ts @@ -0,0 +1,60 @@ +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; +}; + +export const asset = ( + asset: string | AssetObjectValue, + 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; + } + + const invalid = Object.entries({ + code: assetCode(code, type, isRequired), + issuer: publicKey(issuer, isRequired), + }).reduce((res, cur) => { + const [key, value] = cur; + + if (value) { + return { ...res, [key]: value }; + } + + return res; + }, {}); + + return isEmptyObject(invalid) ? false : invalid; +}; diff --git a/src/validate/methods/assetCode.ts b/src/validate/methods/assetCode.ts new file mode 100644 index 00000000..aee5c54b --- /dev/null +++ b/src/validate/methods/assetCode.ts @@ -0,0 +1,36 @@ +import { AssetType } from "@/types/types"; + +export const assetCode = ( + code: string, + assetType: AssetType | undefined, + isRequired?: boolean, +) => { + if (isRequired && !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 false; +}; diff --git a/src/validate/methods/positiveInt.ts b/src/validate/methods/positiveInt.ts new file mode 100644 index 00000000..a0702b43 --- /dev/null +++ b/src/validate/methods/positiveInt.ts @@ -0,0 +1,9 @@ +export const positiveInt = (value: string) => { + if (value.charAt(0) === "-") { + return "Expected a positive number or zero."; + } else if (!value.match(/^[0-9]*$/g)) { + return "Expected a whole number."; + } + + return false; +}; diff --git a/src/validate/methods/publicKey.ts b/src/validate/methods/publicKey.ts new file mode 100644 index 00000000..deaae556 --- /dev/null +++ b/src/validate/methods/publicKey.ts @@ -0,0 +1,21 @@ +import { StrKey } from "stellar-sdk"; + +export const publicKey = (publicKey: string, isRequired?: boolean) => { + if (!publicKey) { + if (isRequired) { + return "Asset issuer is required."; + } else { + return false; + } + } + + if (publicKey.startsWith("M")) { + if (!StrKey.isValidMed25519PublicKey(publicKey)) { + return "Muxed account address is invalid."; + } + } else if (!StrKey.isValidEd25519PublicKey(publicKey)) { + return "Public key is invalid."; + } + + return false; +}; From 3f86946cdb7fd78d7effb0574b7750f4e5ef7ef0 Mon Sep 17 00:00:00 2001 From: Iveta Date: Tue, 19 Mar 2024 14:24:46 -0400 Subject: [PATCH 2/8] 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; From 77aa3ac4233db4aafd0b5c8d4a68275acbb32b24 Mon Sep 17 00:00:00 2001 From: Iveta Date: Tue, 19 Mar 2024 14:50:42 -0400 Subject: [PATCH 3/8] Cleanup --- src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index 8107b0cf..5254eb5b 100644 --- a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx @@ -16,7 +16,6 @@ import { import { InfoCards } from "@/components/InfoCards"; import { TabView } from "@/components/TabView"; import { SdsLink } from "@/components/SdsLink"; -import { Routes } from "@/constants/routes"; import { WithInfoText } from "@/components/WithInfoText"; import { useStore } from "@/store/useStore"; @@ -25,6 +24,7 @@ import { sanitizeArray } from "@/helpers/sanitizeArray"; import { sanitizeObject } from "@/helpers/sanitizeObject"; import { parseJsonString } from "@/helpers/parseJsonString"; +import { Routes } from "@/constants/routes"; import { EXPLORE_ENDPOINTS_PAGES_HORIZON } from "@/constants/exploreEndpointsPages"; import { formComponentTemplate } from "@/constants/formComponentTemplate"; import { AnyObject, AssetObject, Network } from "@/types/types"; From 7a52b31c691e14555774bb17ef769795c733f3b3 Mon Sep 17 00:00:00 2001 From: Iveta Date: Tue, 19 Mar 2024 15:39:51 -0400 Subject: [PATCH 4/8] Remove none option + update anim transition --- .../explore-endpoints/[[...pages]]/page.tsx | 4 ---- src/components/ExpandBox/styles.scss | 2 +- src/components/FormElements/AssetPicker.tsx | 17 ----------------- src/constants/exploreEndpointsPages.ts | 1 - src/constants/formComponentTemplate.tsx | 1 - src/styles/globals.scss | 18 ++++++++++++------ src/types/types.ts | 1 - src/validate/methods/asset.ts | 2 +- 8 files changed, 14 insertions(+), 32 deletions(-) diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index 5254eb5b..c7b91596 100644 --- a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx @@ -147,10 +147,6 @@ export default function ExploreEndpoints() { return "native"; } - if (value.type === "none") { - return false; - } - return `${value.code}:${value.issuer}`; } diff --git a/src/components/ExpandBox/styles.scss b/src/components/ExpandBox/styles.scss index 46816284..09ddd20f 100644 --- a/src/components/ExpandBox/styles.scss +++ b/src/components/ExpandBox/styles.scss @@ -1,7 +1,7 @@ .ExpandBox { display: grid; grid-template-rows: 0fr; - transition: grid-template-rows 200ms ease-out; + transition: grid-template-rows 100ms ease-out; &[data-is-expanded="true"] { grid-template-rows: 1fr; diff --git a/src/components/FormElements/AssetPicker.tsx b/src/components/FormElements/AssetPicker.tsx index 6c700cfe..23786616 100644 --- a/src/components/FormElements/AssetPicker.tsx +++ b/src/components/FormElements/AssetPicker.tsx @@ -17,7 +17,6 @@ type AssetPickerProps = { onChange: (asset: AssetObjectValue | undefined) => void; assetInput: "issued" | "alphanumeric"; fitContent?: boolean; - includeNone?: boolean; includeNative?: boolean; }; @@ -30,7 +29,6 @@ export const AssetPicker = ({ onChange, assetInput, fitContent, - includeNone, includeNative = true, }: AssetPickerProps) => { let options: AssetObject[] = []; @@ -50,21 +48,6 @@ export const AssetPicker = ({ ]; } - if (includeNone) { - options = [ - { - id: "none", - label: "None", - value: { - type: "none", - code: "", - issuer: "", - }, - }, - ...options, - ]; - } - if (assetInput === "alphanumeric") { options = [ ...options, diff --git a/src/constants/exploreEndpointsPages.ts b/src/constants/exploreEndpointsPages.ts index ac3564da..ca5bc736 100644 --- a/src/constants/exploreEndpointsPages.ts +++ b/src/constants/exploreEndpointsPages.ts @@ -47,7 +47,6 @@ export const EXPLORE_ENDPOINTS_PAGES_HORIZON: ExploreEndpointsPagesProps = { custom: { asset: { assetInput: "issued", - includeNone: true, includeNative: true, }, }, diff --git a/src/constants/formComponentTemplate.tsx b/src/constants/formComponentTemplate.tsx index bcba4891..2ef7355e 100644 --- a/src/constants/formComponentTemplate.tsx +++ b/src/constants/formComponentTemplate.tsx @@ -71,7 +71,6 @@ export const formComponentTemplate = ( labelSuffix={!templ.isRequired ? "optional" : undefined} value={parseJsonString(templ.value)} error={templ.error} - includeNone={custom?.includeNone} includeNative={custom?.includeNative} onChange={templ.onChange} /> diff --git a/src/styles/globals.scss b/src/styles/globals.scss index e9b8e978..4491da7f 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -179,7 +179,8 @@ color: var(--Nav-navLink-color); text-decoration: none; padding: pxToRem(12px) 0; - transition: color var(--sds-anim-transition-default), + transition: + color var(--sds-anim-transition-default), border-color var(--sds-anim-transition-default); border-bottom: 2px solid var(--Nav-navLink-border-color); white-space: nowrap; @@ -205,7 +206,8 @@ line-height: pxToRem(20px); font-weight: var(--sds-fw-medium); color: var(--SidebarLink-color); - transition: color var(--sds-anim-transition-default), + transition: + color var(--sds-anim-transition-default), font-weight var(--sds-anim-transition-default); text-decoration: none; display: flex; @@ -242,9 +244,11 @@ } // Highlight if nested link is selected - &:has(~ .SidebarLink__nestedItemsWrapper - > .SidebarLink__nestedItems - > [data-is-active="true"]) { + &:has( + ~ .SidebarLink__nestedItemsWrapper + > .SidebarLink__nestedItems + > [data-is-active="true"] + ) { --SidebarLink-color: var(--sds-clr-gray-12); font-weight: var(--sds-fw-semi-bold); } @@ -265,7 +269,9 @@ &__nestedItemsWrapper { display: grid; grid-template-rows: 0fr; - transition: grid-template-rows 0.5s ease-out, margin-top 0.5s ease-out; + transition: + grid-template-rows 100ms ease-out, + margin-top 100ms ease-out; margin-left: pxToRem(24px); margin-top: pxToRem(-8px); diff --git a/src/types/types.ts b/src/types/types.ts index 1297fbfa..c14691f5 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -45,7 +45,6 @@ export type StatusPageScheduled = { // Asset // ============================================================================= export type AssetType = - | "none" | "native" | "issued" | "credit_alphanum4" diff --git a/src/validate/methods/asset.ts b/src/validate/methods/asset.ts index 89ce7892..cb3c1e85 100644 --- a/src/validate/methods/asset.ts +++ b/src/validate/methods/asset.ts @@ -7,7 +7,7 @@ export const asset = ( asset: AssetObjectValue | undefined, isRequired?: boolean, ) => { - if (asset?.type && ["none", "native"].includes(asset.type)) { + if (asset?.type && asset.type === "native") { return false; } From d87c7d5c014805bde3123b4546b3c0e37146467d Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 20 Mar 2024 09:41:03 -0400 Subject: [PATCH 5/8] Clean up input onChange methods --- .../explore-endpoints/[[...pages]]/page.tsx | 66 +++++++------------ 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index c7b91596..171889eb 100644 --- a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx @@ -234,6 +234,25 @@ export default function ExploreEndpoints() { if (component) { const isRequired = requiredFields.includes(f); + // storeValue is saved in the store and may need special + // formatting (sanitizing object or array, for exmaple). + // Error check needs the original value. + const handleChange = (value: any, storeValue: any) => { + updateParams({ + [f]: storeValue, + }); + + const error = component.validate?.(value, isRequired); + + if (error) { + setFormError({ ...formError, [f]: error }); + } else if (formError[f]) { + const updatedErrors = { ...formError }; + delete updatedErrors[f]; + setFormError(updatedErrors); + } + }; + switch (f) { case "asset": return component.render({ @@ -241,22 +260,12 @@ export default function ExploreEndpoints() { error: formError[f], isRequired, onChange: (assetObj: AssetObject) => { - updateParams({ - [f]: isEmptyObject(sanitizeObject(assetObj || {})) + handleChange( + assetObj, + 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": @@ -265,18 +274,7 @@ export default function ExploreEndpoints() { 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); - } - } + handleChange(optionId, optionId); }, }); default: @@ -285,21 +283,7 @@ export default function ExploreEndpoints() { 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); - } - } + handleChange(e.target.value, e.target.value); }, }); } From 3f387e9f6fa9fa5302ec426984161cf400b18bfd Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 20 Mar 2024 10:33:26 -0400 Subject: [PATCH 6/8] Require code and issuer for non-native assets --- .../explore-endpoints/[[...pages]]/page.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index 171889eb..7882c4f3 100644 --- a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx @@ -60,6 +60,11 @@ export default function ExploreEndpoints() { const [requestUrl, setRequestUrl] = useState(""); const isSubmitEnabled = () => { + let isValidReqFields = true; + let isValidReqAssetFields = true; + let isValid = true; + + // Checking if all required fields have values const missingReqFields = requiredFields.reduce((res, cur) => { if (!params[cur]) { return [...res, cur]; @@ -68,11 +73,25 @@ export default function ExploreEndpoints() { return res; }, [] as string[]); - if (missingReqFields.length !== 0) { - return false; + isValidReqFields = missingReqFields.length === 0; + + // Checking if there are any errors + isValid = isEmptyObject(formError); + + // When non-native asset is selected, code and issuer fields are required + if (params.asset) { + const assetObj = parseJsonString(params.asset); + + if ( + ["issued", "credit_alphanum4", "credit_alphanum12"].includes( + assetObj.type, + ) + ) { + isValidReqAssetFields = Boolean(assetObj.code && assetObj.issuer); + } } - return isEmptyObject(formError); + return isValidReqAssetFields && isValidReqFields && isValid; }; useEffect(() => { From 730d33f98c77535e47be866a8cd283d0c391612b Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 20 Mar 2024 10:55:41 -0400 Subject: [PATCH 7/8] Text updates --- .../explore-endpoints/[[...pages]]/page.tsx | 46 +++++++++++++++---- src/app/page.tsx | 24 ++++++---- src/components/MainNav.tsx | 2 +- .../layout/LayoutSidebarContent.tsx | 5 +- 4 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx index 7882c4f3..3fc5dfe5 100644 --- a/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx @@ -10,6 +10,7 @@ import { CopyText, Icon, Input, + Link, Text, } from "@stellar/design-system"; @@ -17,6 +18,7 @@ import { InfoCards } from "@/components/InfoCards"; import { TabView } from "@/components/TabView"; import { SdsLink } from "@/components/SdsLink"; import { WithInfoText } from "@/components/WithInfoText"; +import { NextLink } from "@/components/NextLink"; import { useStore } from "@/store/useStore"; import { isEmptyObject } from "@/helpers/isEmptyObject"; @@ -357,7 +359,7 @@ export default function ExploreEndpoints() { This tool can be used to run queries against the{" "} - + REST API endpoints {" "} on the Horizon server. Horizon is the client facing library for the @@ -370,23 +372,26 @@ export default function ExploreEndpoints() { const ExploreEndpointsLandingPage = () => { const infoCards = [ { - id: "soroban-rpc", - title: "RPC Endpoints", - description: "TODO: add text", + id: "stellar-rpc", + title: "Stellar RPC Endpoints", + description: "Learn about the RPC endpoints in our Developer Docs.", buttonLabel: "See docs", buttonIcon: , buttonAction: () => - window.open("https://soroban.stellar.org/api/methods", "_blank"), + window.open( + "https://developers.stellar.org/network/soroban-rpc/methods", + "_blank", + ), }, { id: "horizon", title: "Horizon Endpoints", - description: "TODO: add text", + description: "Learn about the Horizon endpoints in our Developer Docs.", buttonLabel: "See docs", buttonIcon: , buttonAction: () => window.open( - "https://developers.stellar.org/api/horizon/resources/", + "https://developers.stellar.org/network/horizon/resources", "_blank", ), }, @@ -397,11 +402,34 @@ const ExploreEndpointsLandingPage = () => {
- Endpoints + Explore Endpoints + + + + The Stellar Laboratory is a set of tools that enables people to try + out and learn about the Stellar network. The Laboratory can{" "} + + build transactions + + ,{" "} + + sign them + + , and{" "} + + submit them to the network + + . In this section of the Laboratory, you can explore the various + endpoints from the RPC and Horizon, make requests to these + endpoints, and save them for future use. - TODO: add text + For Stellar docs, take a look at the{" "} + + Stellar developers site + + .
diff --git a/src/app/page.tsx b/src/app/page.tsx index c5222128..be4f8959 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -24,31 +24,34 @@ export default function Introduction() { }, { id: "tools", - title: "Tools", + title: "Developer Tools", description: - "Tools for reading and interacting with Soroban smart contracts on the Stellar Network", + "Tools, like the Stellar CLI, for reading and interacting with smart contracts on the Stellar Network", buttonLabel: "See tools", buttonIcon: undefined, buttonAction: () => router.push(Routes.SOROBAN_CONTRACT_EXPLORER), }, { - id: "soroban-rpc", - title: "Learn about the Soroban RPC", - description: "Learn about the Soroban RPC, a RPC gateway to Stellar", + id: "stellar-rpc", + title: "Stellar RPC", + description: "Learn about the Stellar RPC, a RPC gateway to Stellar", buttonLabel: "Go to docs", buttonIcon: , buttonAction: () => - window.open("https://soroban.stellar.org/docs/reference/rpc", "_blank"), + window.open( + "https://developers.stellar.org/network/soroban-rpc", + "_blank", + ), }, { id: "horizon", - title: "Learn about Horizon", + title: "Horizon", description: "Learn about the Horizon for interacting with the Stellar network", buttonLabel: "Go to docs", buttonIcon: , buttonAction: () => - window.open("https://developers.stellar.org/api/horizon", "_blank"), + window.open("https://developers.stellar.org/network/horizon", "_blank"), }, ]; @@ -62,7 +65,7 @@ export default function Introduction() { The Stellar Laboratory is a set of tools that enables people to try - out and learn about the Stellar network. The laboratory can{" "} + out and learn about the Stellar network. The Laboratory can{" "} build transactions @@ -74,7 +77,8 @@ export default function Introduction() { submit them to the network - . It can also make requests to any of the Horizon endpoints. + . It can also make requests to RPC and Horizon endpoints. You can + save your transactions and runbooks for future use. diff --git a/src/components/MainNav.tsx b/src/components/MainNav.tsx index d6577a70..233b3abe 100644 --- a/src/components/MainNav.tsx +++ b/src/components/MainNav.tsx @@ -37,7 +37,7 @@ const primaryNavLinks: NavLink[] = [ const secondaryNavLinks: NavLink[] = [ { - href: "https://developers.stellar.org/docs", + href: "https://developers.stellar.org/network", label: "View Documentation", }, ]; diff --git a/src/components/layout/LayoutSidebarContent.tsx b/src/components/layout/LayoutSidebarContent.tsx index 501b76e7..c28bc802 100644 --- a/src/components/layout/LayoutSidebarContent.tsx +++ b/src/components/layout/LayoutSidebarContent.tsx @@ -69,7 +69,10 @@ export const LayoutSidebarContent = ({ ))}
- + Got product feedback?
From eaa1d50d654da72b2928fdde00c7e9f4cce62ab6 Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 20 Mar 2024 11:01:43 -0400 Subject: [PATCH 8/8] Fix tests --- tests/introductionPage.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/introductionPage.test.ts b/tests/introductionPage.test.ts index f53f6994..98112de6 100644 --- a/tests/introductionPage.test.ts +++ b/tests/introductionPage.test.ts @@ -12,9 +12,9 @@ test.describe("Network selector", () => { test("Renders info cards", async ({ page }) => { await expect(page.locator("h2")).toHaveText([ "Stellar Quest", - "Tools", - "Learn about the Soroban RPC", - "Learn about Horizon", + "Developer Tools", + "Stellar RPC", + "Horizon", ]); }); });