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/App.tsx b/ui/src/App.tsx index 04fa0a3..8d5a1e5 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>; @@ -36,23 +27,6 @@ const ENDPOINTS = { const watcher = makeAgoricChainStorageWatcher(ENDPOINTS.API, '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; @@ -127,20 +101,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(); @@ -167,179 +127,24 @@ function App() { }); }; - const Logos = () => ( - <> -
- - Vite logo - - - React logo - - - Agoric logo - -
- - ); - - 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/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/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..9ce8b87 --- /dev/null +++ b/ui/src/components/Trade.tsx @@ -0,0 +1,141 @@ +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, 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); +}; + +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: 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); + }; + + return ( + <> +
+

Want: Choose up to 3 items

+
+ {entries(nameToIcon).map(([title, icon]) => ( + + ))} +
+
+
+

Give: Offer at least 0.25 IST

+
+ + setGiveValue(parseValue(ev?.target?.value, istPurse)) + } + inputClassName={giveValue >= terms.price ? 'ok' : 'error'} + inputStep="0.01" + /> +
+
+
+ {walletConnected && ( + + )} +
+ + ); +}; + +export { Trade }; diff --git a/ui/src/index.css b/ui/src/index.css index 00bfa74..d3f626a 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -26,7 +26,6 @@ a:hover { body { margin: 0; display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } @@ -38,8 +37,8 @@ h1 { button { border-radius: 8px; border: 4px solid transparent; - padding: 16px; - margin: 4px 2px; + padding: 12px 16px; + margin: 8px 2px; font-size: 1em; font-weight: 500; font-family: inherit; @@ -48,7 +47,7 @@ button { transition: border-color 0.25s; } button:hover { - border-color: #646cff; + background: #171717; } button:focus, button:focus-visible { @@ -65,5 +64,9 @@ button:focus-visible { } button { background-color: #04aa6d; /* Green */ + color: #fff; + } + button:hover { + background: #039962; } } 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; + }; +}