From a641d5298daf6f1a8efdbb882f745024e1b52786 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 26 Jan 2024 18:51:43 -0500 Subject: [PATCH 1/2] refactor: split into components --- ui/src/App.tsx | 229 +++----------------------------- ui/src/components/Inventory.tsx | 49 +++++++ ui/src/components/Logos.tsx | 19 +++ ui/src/components/Trade.tsx | 140 +++++++++++++++++++ ui/src/index.d.ts | 16 +++ 5 files changed, 241 insertions(+), 212 deletions(-) create mode 100644 ui/src/components/Inventory.tsx create mode 100644 ui/src/components/Logos.tsx create mode 100644 ui/src/components/Trade.tsx create mode 100644 ui/src/index.d.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 74b5c82..0ad9f76 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,10 +1,5 @@ -import { FormEvent, useEffect, useState } from 'react'; -import reactLogo from './assets/react.svg'; -import viteLogo from '/vite.svg'; -import agoricLogo from '/agoric.svg'; -import scrollIcon from './assets/scroll.png'; -import mapIcon from './assets/map.png'; -import potionIcon from './assets/potionBlue.png'; +import { useEffect } from 'react'; + import './App.css'; import { makeAgoricChainStorageWatcher, @@ -16,16 +11,12 @@ import { suggestChain, } from '@agoric/web-components'; import { subscribeLatest } from '@agoric/notifier'; -import { stringifyAmountValue } from '@agoric/ui-components'; import { makeCopyBag } from '@agoric/store'; +import { Logos } from './components/Logos'; +import { Inventory } from './components/Inventory'; +import { Trade } from './components/Trade'; -const { entries, fromEntries, keys, values } = Object; -const sum = (xs: bigint[]) => xs.reduce((acc, next) => acc + next, 0n); - -const terms = { - price: 250000n, - maxItems: 3n, -}; +const { entries, fromEntries } = Object; type Wallet = Awaited>; @@ -34,23 +25,6 @@ const watcher = makeAgoricChainStorageWatcher( 'agoriclocal', ); -interface CopyBag { - payload: Array<[T, bigint]>; -} - -interface Purse { - brand: unknown; - brandPetname: string; - currentAmount: { - brand: unknown; - value: bigint | CopyBag; - }; - displayInfo: { - decimalPlaces: number; - assetKind: unknown; - }; -} - interface AppState { wallet?: Wallet; offerUpInstance?: unknown; @@ -125,20 +99,6 @@ const makeOffer = (giveValue: bigint, wantChoices: Record) => { ); }; -const nameToIcon = { - scroll: scrollIcon, - map: mapIcon, - potion: potionIcon, -} as const; -type ItemName = keyof typeof nameToIcon; -type ItemChoices = Partial>; - -const parseValue = (numeral: string, purse: Purse): bigint => { - const { decimalPlaces } = purse.displayInfo; - const num = Number(numeral) * 10 ** decimalPlaces; - return BigInt(num); -}; - function App() { useEffect(() => { setup(); @@ -165,179 +125,24 @@ function App() { }); }; - const Logos = () => ( - <> - - - ); - - const Inventory = () => - wallet && - istPurse && ( -
-

My Wallet

-
-
- - {wallet.address} - -
- -
-
- IST: - {stringifyAmountValue( - istPurse.currentAmount, - istPurse.displayInfo.assetKind, - istPurse.displayInfo.decimalPlaces, - )} -
-
- Items: - {itemsPurse ? ( -
    - {(itemsPurse.currentAmount.value as CopyBag).payload.map( - ([name, number]) => ( -
  • - {String(number)} {name} -
  • - ), - )} -
- ) : ( - 'None' - )} -
-
-
-
- ); - - // XXX giveValue, choices state should be scoped to Trade component. - const [giveValue, setGiveValue] = useState(terms.price); - const renderGiveValue = (purse: Purse) => ( - setGiveValue(parseValue(ev?.target?.value, purse))} - className={giveValue >= terms.price ? 'ok' : 'error'} - step="0.01" - /> - ); - - const [choices, setChoices] = useState({ map: 1n, scroll: 2n }); - const changeChoice = (ev: FormEvent) => { - if (!ev.target) return; - const elt = ev.target as HTMLInputElement; - const title = elt.title as ItemName; - if (!title) return; - const qty = BigInt(elt.value); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [title]: _old, ...rest }: ItemChoices = choices; - const newChoices = qty > 0 ? { ...rest, [title]: qty } : rest; - setChoices(newChoices); - }; - - const WantItems = () => ( - <> - - - Want: Choose up to 3 items - - - - - {entries(nameToIcon).map(([title, icon]) => ( - - - - ))} - - - - {keys(nameToIcon).map(title => ( - - -
- {title} - - ))} - - - - ); - - // TODO: don't wait for connect wallet to show Give. - // IST displayInfo is available in vbankAsset or boardAux - const Trade = () => ( - <> - - - {istPurse && ( - <> - - - - - - - - - - - - - - )} -
- Give: Offer at least 0.25 IST -
{renderGiveValue(istPurse)}IST
-
- {wallet && ( - - )} -
- - ); - return ( <>

Items Listed on Offer Up

- +
- {wallet ? ( - + {wallet && istPurse ? ( + ) : ( )} diff --git a/ui/src/components/Inventory.tsx b/ui/src/components/Inventory.tsx new file mode 100644 index 0000000..53dea99 --- /dev/null +++ b/ui/src/components/Inventory.tsx @@ -0,0 +1,49 @@ +import { stringifyAmountValue } from '@agoric/ui-components'; + +type InventoryProps = { + address: string; + istPurse: Purse; + itemsPurse: Purse; +}; + +const Inventory = ({ address, istPurse, itemsPurse }: InventoryProps) => ( +
+

My Wallet

+
+
+ + {address} + +
+ +
+
+ IST: + {stringifyAmountValue( + istPurse.currentAmount, + istPurse.displayInfo.assetKind, + istPurse.displayInfo.decimalPlaces, + )} +
+
+ Items: + {itemsPurse ? ( +
    + {(itemsPurse.currentAmount.value as CopyBag).payload.map( + ([name, number]) => ( +
  • + {String(number)} {name} +
  • + ), + )} +
+ ) : ( + 'None' + )} +
+
+
+
+); + +export { Inventory }; diff --git a/ui/src/components/Logos.tsx b/ui/src/components/Logos.tsx new file mode 100644 index 0000000..13e9e5c --- /dev/null +++ b/ui/src/components/Logos.tsx @@ -0,0 +1,19 @@ +import reactLogo from '../assets/react.svg'; +import viteLogo from '/vite.svg'; +import agoricLogo from '/agoric.svg'; + +const Logos = () => ( + +); + +export { Logos }; diff --git a/ui/src/components/Trade.tsx b/ui/src/components/Trade.tsx new file mode 100644 index 0000000..8f01eb6 --- /dev/null +++ b/ui/src/components/Trade.tsx @@ -0,0 +1,140 @@ +import { FormEvent, useState } from 'react'; +import { stringifyAmountValue } from '@agoric/ui-components'; +import scrollIcon from '../assets/scroll.png'; +import mapIcon from '../assets/map.png'; +import potionIcon from '../assets/potionBlue.png'; + +const { entries, keys, values } = Object; +const sum = (xs: bigint[]) => xs.reduce((acc, next) => acc + next, 0n); + +const terms = { + price: 250000n, + maxItems: 3n, +}; +const nameToIcon = { + scroll: scrollIcon, + map: mapIcon, + potion: potionIcon, +} as const; +type ItemName = keyof typeof nameToIcon; +type ItemChoices = Partial>; + +const parseValue = (numeral: string, purse: Purse): bigint => { + const { decimalPlaces } = purse.displayInfo; + const num = Number(numeral) * 10 ** decimalPlaces; + return BigInt(num); +}; + +type TradeProps = { + makeOffer: (giveValue: bigint, wantChoices: Record) => void; + istPurse: Purse; + walletConnected: boolean; +}; + +// TODO: don't wait for connect wallet to show Give. +// IST displayInfo is available in vbankAsset or boardAux +const Trade = ({ makeOffer, istPurse, walletConnected }: TradeProps) => { + const [giveValue, setGiveValue] = useState(terms.price); + const [choices, setChoices] = useState({ map: 1n, scroll: 2n }); + const changeChoice = (ev: FormEvent) => { + if (!ev.target) return; + const elt = ev.target as HTMLInputElement; + const title = elt.title as ItemName; + if (!title) return; + const qty = BigInt(elt.value); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [title]: _old, ...rest }: ItemChoices = choices; + const newChoices = qty > 0 ? { ...rest, [title]: qty } : rest; + setChoices(newChoices); + }; + + const renderGiveValue = (purse: Purse) => ( + setGiveValue(parseValue(ev?.target?.value, purse))} + className={giveValue >= terms.price ? 'ok' : 'error'} + step="0.01" + /> + ); + + const WantItems = () => ( + <> + + + Want: Choose up to 3 items + + + + + {entries(nameToIcon).map(([title, icon]) => ( + + + + ))} + + + + {keys(nameToIcon).map(title => ( + + +
+ {title} + + ))} + + + + ); + + return ( + <> + + + {istPurse && ( + <> + + + + + + + + + + + + + + )} +
+ Give: Offer at least 0.25 IST +
{renderGiveValue(istPurse)}IST
+
+ {walletConnected && ( + + )} +
+ + ); +}; + +export { Trade }; diff --git a/ui/src/index.d.ts b/ui/src/index.d.ts new file mode 100644 index 0000000..d2ee4b3 --- /dev/null +++ b/ui/src/index.d.ts @@ -0,0 +1,16 @@ +interface CopyBag { + payload: Array<[T, bigint]>; +} + +interface Purse { + brand: unknown; + brandPetname: string; + currentAmount: { + brand: unknown; + value: bigint | CopyBag; + }; + displayInfo: { + decimalPlaces: number; + assetKind: unknown; + }; +} From ed60c8792cec38a23acc1023102b1c8e85c45ba8 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 26 Jan 2024 19:11:10 -0500 Subject: [PATCH 2/2] feat(ui): improve styling --- ui/src/App.css | 57 +++++++++++-- ui/src/assets/IST.svg | 19 +++++ ui/src/components/Trade.tsx | 157 ++++++++++++++++++------------------ ui/src/index.css | 11 ++- 4 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 ui/src/assets/IST.svg diff --git a/ui/src/App.css b/ui/src/App.css index 4bebad4..b4d6ec7 100644 --- a/ui/src/App.css +++ b/ui/src/App.css @@ -49,14 +49,61 @@ border-radius: 10%; } -.want { - border-collapse: collapse; +.coin { + width: 2em; + margin: 10px; } -.want td { - border: 1px solid; +.trade { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: #171717; + border-radius: 25px; + margin-bottom: 15px; +} + +.item-col { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 15px 25px 15px; + margin: 5px; +} + +.row-center { + display: flex; + flex-direction: row; + align-items: center; +} + +input { + border: none; + background: #242424; + text-align: center; + padding: 5px 10px; + border-radius: 15px; + font-size: 1.2rem; + width: 75px; +} + +@media (prefers-color-scheme: light) { + .trade { + background: #fafafa; + border: 1px solid #e5e5e5; + } + input { + background: #e5e5e5; + } } .error { - background-color: red; + background-color: #E11D48; + color: #fff; +} + +/* increment/decrement arrows always visible */ +input[type=number]::-webkit-inner-spin-button { + opacity: 1 } diff --git a/ui/src/assets/IST.svg b/ui/src/assets/IST.svg new file mode 100644 index 0000000..1fcb75c --- /dev/null +++ b/ui/src/assets/IST.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/components/Trade.tsx b/ui/src/components/Trade.tsx index 8f01eb6..9ce8b87 100644 --- a/ui/src/components/Trade.tsx +++ b/ui/src/components/Trade.tsx @@ -1,10 +1,11 @@ import { FormEvent, useState } from 'react'; import { stringifyAmountValue } from '@agoric/ui-components'; import scrollIcon from '../assets/scroll.png'; +import istIcon from '../assets/IST.svg'; import mapIcon from '../assets/map.png'; import potionIcon from '../assets/potionBlue.png'; -const { entries, keys, values } = Object; +const { entries, values } = Object; const sum = (xs: bigint[]) => xs.reduce((acc, next) => acc + next, 0n); const terms = { @@ -25,14 +26,49 @@ const parseValue = (numeral: string, purse: Purse): bigint => { return BigInt(num); }; +const Item = ({ + icon, + coinIcon, + label, + value, + onChange, + inputClassName, + inputStep, +}: { + icon?: string; + coinIcon?: string; + label: string; + value: number | string; + onChange: React.ChangeEventHandler; + inputClassName: string; + inputStep?: string; +}) => ( +
+ + {icon && } + {coinIcon && } + +
+); + type TradeProps = { makeOffer: (giveValue: bigint, wantChoices: Record) => void; istPurse: Purse; walletConnected: boolean; }; -// TODO: don't wait for connect wallet to show Give. -// IST displayInfo is available in vbankAsset or boardAux +// TODO: IST displayInfo is available in vbankAsset or boardAux const Trade = ({ makeOffer, istPurse, walletConnected }: TradeProps) => { const [giveValue, setGiveValue] = useState(terms.price); const [choices, setChoices] = useState({ map: 1n, scroll: 2n }); @@ -48,84 +84,49 @@ const Trade = ({ makeOffer, istPurse, walletConnected }: TradeProps) => { setChoices(newChoices); }; - const renderGiveValue = (purse: Purse) => ( - setGiveValue(parseValue(ev?.target?.value, purse))} - className={giveValue >= terms.price ? 'ok' : 'error'} - step="0.01" - /> - ); - - const WantItems = () => ( + return ( <> - - - Want: Choose up to 3 items - - - - +
+

Want: Choose up to 3 items

+
{entries(nameToIcon).map(([title, icon]) => ( - - - - ))} - - - - {keys(nameToIcon).map(title => ( - - -
- {title} - + ))} - - - - ); - - return ( - <> - - - {istPurse && ( - <> - - - - - - - - - - - - - - )} -
- Give: Offer at least 0.25 IST -
{renderGiveValue(istPurse)}IST
+
+
+
+

Give: Offer at least 0.25 IST

+
+ + setGiveValue(parseValue(ev?.target?.value, istPurse)) + } + inputClassName={giveValue >= terms.price ? 'ok' : 'error'} + inputStep="0.01" + /> +
+
{walletConnected && (