Skip to content

Commit

Permalink
Explore Endpoints: data flow functional
Browse files Browse the repository at this point in the history
  • Loading branch information
quietbits committed Mar 15, 2024
1 parent a126002 commit 7ae0e66
Show file tree
Hide file tree
Showing 11 changed files with 404 additions and 4 deletions.
167 changes: 164 additions & 3 deletions src/app/(sidebar)/explore-endpoints/[[...pages]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import {
Alert,
Expand All @@ -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<AnyObject>({});
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 <ExploreEndpointsLandingPage />;
Expand All @@ -46,8 +151,12 @@ export default function ExploreEndpoints() {
// TODO: set request type
leftElement={<div className="Endpoints__input__requestType">GET</div>}
/>
{/* TODO: disable if can't submit */}
<Button size="md" variant="secondary" type="submit">
<Button
size="md"
variant="secondary"
type="submit"
disabled={!isSubmitEnabled()}
>
Submit
</Button>
{/* TODO: add text to copy */}
Expand All @@ -64,6 +173,58 @@ export default function ExploreEndpoints() {
<div className="Endpoints__content__inputs">
{/* TODO: render fields for path */}
{`Explore Endpoints: ${pathname}`}

<div>
<Input
label="Sponsor"
id="sponsor"
fieldSize="md"
value={params.sponsor || ""}
onChange={(e) => {
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}
/>

<Input
label="Signer"
id="signer"
fieldSize="md"
value={params.signer || ""}
onChange={(e) => {
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}
/>
</div>
</div>

<WithInfoText href="https://developers.stellar.org/network/horizon/structure/streaming">
Expand Down
5 changes: 5 additions & 0 deletions src/helpers/isEmptyObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AnyObject } from "@/types/types";

export const isEmptyObject = (obj: AnyObject) => {
return Object.keys(obj).length === 0;
};
13 changes: 13 additions & 0 deletions src/helpers/sanitizeObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AnyObject } from "@/types/types";

export const sanitizeObject = <T extends AnyObject>(obj: T) => {
return Object.keys(obj).reduce((res, param) => {
const paramValue = obj[param];

if (paramValue) {
return { ...res, [param]: paramValue };
}

return res;
}, {} as T);
};
11 changes: 11 additions & 0 deletions src/hooks/usePrevious.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useEffect, useRef } from "react";

export const usePrevious = <T>(value: T) => {
const ref = useRef<T>();

useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;
};
74 changes: 73 additions & 1 deletion src/store/createStore.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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<Store>()(
// 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: {
Expand All @@ -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,
Expand All @@ -68,6 +136,10 @@ export const createStore = (options: CreateStoreOptions) =>
return {
network: true,
account: true,
exploreEndpoints: {
params: true,
isStreaming: true,
},
};
},
key: "||",
Expand Down
1 change: 1 addition & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// =============================================================================
// Generic
// =============================================================================
export type AnyObject = { [key: string]: any };
export type EmptyObj = Record<PropertyKey, never>;

// =============================================================================
Expand Down
11 changes: 11 additions & 0 deletions src/validate/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
Loading

0 comments on commit 7ae0e66

Please sign in to comment.