diff --git a/package.json b/package.json index 9ca808dc..b67d537d 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,12 @@ }, "dependencies": { "@stellar/design-system": "^2.0.0-beta.1", + "immer": "^10.0.3", "next": "14.0.4", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "zustand": "^4.5.0", + "zustand-querystring": "^0.0.19" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/(sidebar)/account/create/page.tsx b/src/app/(sidebar)/account/create/page.tsx index 3aa44803..feb52d34 100644 --- a/src/app/(sidebar)/account/create/page.tsx +++ b/src/app/(sidebar)/account/create/page.tsx @@ -1,5 +1,14 @@ "use client"; +import { useStore } from "@/store/useStore"; + export default function CreateAccount() { - return
Create Account
; + const { network } = useStore(); + + return ( +
+ Create Account +
{`Current network: ${network?.id}`}
+
+ ); } diff --git a/src/app/(sidebar)/account/fund/page.tsx b/src/app/(sidebar)/account/fund/page.tsx index afb29bdc..07df3f55 100644 --- a/src/app/(sidebar)/account/fund/page.tsx +++ b/src/app/(sidebar)/account/fund/page.tsx @@ -1,5 +1,74 @@ "use client"; +import { useStore } from "@/store/useStore"; +import { Input, Button, Select } from "@stellar/design-system"; +import { useState } from "react"; + export default function FundAccount() { - return
Fund Account
; + const { + account: { value, update, updateNested, reset }, + } = useStore(); + + const [testValue, setTestValue] = useState(value); + + const handleUpdateNested = (event: any) => { + const val = event.target.value; + let submitValue = { + nestedValue1: "AAA", + nestedValue2: 111, + }; + + if (val === "Two") { + submitValue = { + nestedValue1: "BBB", + nestedValue2: 222, + }; + } + + updateNested(submitValue); + }; + + return ( +
+ Fund Account +
+
{`Test value: ${value}`}
+ +

Store value is updated on blur

+ + setTestValue(event.target.value)} + onBlur={(event) => update(event.target.value)} + label="Value" + /> + +

Testing nested object update

+ + + + +
+
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d4fca4bc..c196d181 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { LayoutMain } from "@/components/layout/LayoutMain"; +import { StoreProvider } from "@/store/StoreProvider"; import "@stellar/design-system/build/styles.min.css"; import "@/styles/globals.scss"; @@ -20,7 +21,9 @@ export default function RootLayout({
- {children} + + {children} +
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 0625ac2c..356ffb86 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -6,6 +6,7 @@ import { Routes } from "@/constants/routes"; import { LayoutContentContainer } from "@/components/layout/LayoutContentContainer"; // TODO: update 404 +// TODO: keep searchParams export default function NotFound() { return ( diff --git a/src/components/MainNav.tsx b/src/components/MainNav.tsx index 04bfdf74..f7ee0c26 100644 --- a/src/components/MainNav.tsx +++ b/src/components/MainNav.tsx @@ -34,6 +34,7 @@ export const MainNav = () => { const pathname = usePathname(); const searchParams = useSearchParams(); + // TODO: remove after testing console.log(">>> pathname: ", pathname); console.log(">>> searchParams: ", searchParams.toString()); diff --git a/src/components/NetworkSelector.tsx b/src/components/NetworkSelector.tsx new file mode 100644 index 00000000..b38b2f2a --- /dev/null +++ b/src/components/NetworkSelector.tsx @@ -0,0 +1,52 @@ +"use client"; + +// TODO: update Network selector. This is a demo for state/store. + +import { useCallback, useEffect } from "react"; +import { Select } from "@stellar/design-system"; +import { useStore } from "@/store/useStore"; + +export const NetworkSelector = () => { + const { network, selectNetwork } = useStore(); + + const setNetwork = useCallback(() => { + if (!network?.id) { + // TODO: get from local storage + selectNetwork({ + id: "testnet", + label: "testnet", + url: "", + passphrase: "", + }); + } + }, [network?.id, selectNetwork]); + + // Set default network on launch + useEffect(() => { + setNetwork(); + }, [setNetwork]); + + return ( +
+ {`Network: ${network?.id}`} + +
+ ); +}; diff --git a/src/components/layout/LayoutMain.tsx b/src/components/layout/LayoutMain.tsx index 05fd6127..e89a4eb3 100644 --- a/src/components/layout/LayoutMain.tsx +++ b/src/components/layout/LayoutMain.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import { ProjectLogo, ThemeSwitch } from "@stellar/design-system"; import { MainNav } from "@/components/MainNav"; +import { NetworkSelector } from "@/components/NetworkSelector"; export const LayoutMain = ({ children }: { children: ReactNode }) => { return ( @@ -20,6 +21,7 @@ export const LayoutMain = ({ children }: { children: ReactNode }) => {
+
diff --git a/src/store/StoreProvider.tsx b/src/store/StoreProvider.tsx new file mode 100644 index 00000000..0d545854 --- /dev/null +++ b/src/store/StoreProvider.tsx @@ -0,0 +1,23 @@ +"use client"; +import { createContext, ReactNode, useState } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { createStore } from "@/store/createStore"; + +export type StoreType = ReturnType; +export const ZustandContext = createContext(null); + +export const StoreProvider = ({ children }: { children: ReactNode }) => { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const url = `${pathname}?${searchParams}`; + + const [store] = useState(() => + createStore({ + url, + }), + ); + + return ( + {children} + ); +}; diff --git a/src/store/createStore.ts b/src/store/createStore.ts new file mode 100644 index 00000000..f988df3b --- /dev/null +++ b/src/store/createStore.ts @@ -0,0 +1,83 @@ +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { querystring } from "zustand-querystring"; +import { EmptyObj } from "@/types/types"; + +type Network = { + id: string; + label: string; + url: string; + passphrase: string; +}; + +export interface Store { + // Shared + network: Network | EmptyObj; + selectNetwork: (network: Network) => void; + + // Account + account: { + value: string; + nestedObject: { + nestedValue1: string; + nestedValue2: number; + }; + update: (value: string) => void; + updateNested: (nestedVal: { + nestedValue1: string; + nestedValue2: number; + }) => void; + reset: () => void; + }; +} + +interface CreateStoreOptions { + url?: string; +} + +// Store +export const createStore = (options: CreateStoreOptions) => + create()( + querystring( + immer((set) => ({ + network: {}, + selectNetwork: (network: Network) => + set((state) => { + state.network = network; + }), + account: { + value: "", + nestedObject: { + nestedValue1: "", + nestedValue2: 0, + }, + update: (value: string) => + set((state) => { + state.account.value = value; + }), + updateNested: (nestedVal: { + nestedValue1: string; + nestedValue2: number; + }) => + set((state) => { + state.account.nestedObject = nestedVal; + }), + reset: () => + set((state) => { + state.account.value = ""; + }), + }, + })), + { + url: options.url, + // Select what to save in query string + select() { + return { + network: true, + account: true, + }; + }, + key: "||", + }, + ), + ); diff --git a/src/store/useStore.ts b/src/store/useStore.ts new file mode 100644 index 00000000..3545f3b2 --- /dev/null +++ b/src/store/useStore.ts @@ -0,0 +1,12 @@ +"use client"; +import { useContext } from "react"; +import { useStore as useZustandStore } from "zustand"; +import { Store } from "@/store/createStore"; +import { ZustandContext } from "@/store/StoreProvider"; + +export const useStore = (selector?: (state: Store) => T) => { + selector ??= (state) => state as T; + const store = useContext(ZustandContext); + if (!store) throw new Error("Store is missing the provider"); + return useZustandStore(store, selector); +}; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..f90e5711 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1 @@ +export type EmptyObj = Record; diff --git a/yarn.lock b/yarn.lock index a0e5370b..0fb2bea6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1352,6 +1352,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +immer@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9" + integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A== + immutable@^4.0.0: version "4.3.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" @@ -1717,6 +1722,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -2583,6 +2593,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + watchpack@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" @@ -2708,3 +2723,17 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand-querystring@^0.0.19: + version "0.0.19" + resolved "https://registry.yarnpkg.com/zustand-querystring/-/zustand-querystring-0.0.19.tgz#18ac1c9258a21c6345aa09dbf8738133471c2ebd" + integrity sha512-+lLMisaJOgEiEochoCYKKwodR4Gd6RdIcJu4j2uuM5HE/8YzXVwKEAWc/XnU46ixXT8dj33wpedm0+iWqgjOSQ== + dependencies: + lodash-es "^4.17.21" + +zustand@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.0.tgz#141354af56f91de378aa6c4b930032ab338f3ef0" + integrity sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A== + dependencies: + use-sync-external-store "1.2.0"