Skip to content

Commit

Permalink
feat(minifront): #1211: remove swap restrictions (#1337)
Browse files Browse the repository at this point in the history
* feat(minifront): #1211: extend BalanceSelector to accept metadata assets

* feat(minifront): #1211: apply correct validations for swap action

* chore: changesets

* chore: fix linting

* chore: fix linting

* fix(minifront): #1211: set initial swap assets correctly

* fix(minifront): #1211: make the code cleaner after the review

* fix(ui): #1211: don't let IconInput accept all InputProps
  • Loading branch information
VanishMax authored Jun 21, 2024
1 parent 2f0a8e4 commit 97b7231
Show file tree
Hide file tree
Showing 20 changed files with 345 additions and 202 deletions.
14 changes: 14 additions & 0 deletions .changeset/friendly-horses-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'minifront': minor
'@repo/ui': patch
---

Minifront:

- extend `BalanceSelector` to show not only assets with balances but all available assets
- fix the issues with empty wallets not rendering a swap block correctly
- reduce the height of `BalanceSelecor` and `AssetSelector` to `90dvh`
- autofocus the search inputs in `BalanceSelecor` and `AssetSelector`
- change validations of the swap input to allow entering any possible values

UI: allow passing `autoFocus` attribute to the `IconInput` component
3 changes: 2 additions & 1 deletion apps/minifront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@types/lodash": "^4.17.4",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11"
"@types/react-helmet": "^6.1.11",
"vite": "^5.2.11"
}
}
114 changes: 0 additions & 114 deletions apps/minifront/src/components/shared/balance-selector.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion apps/minifront/src/components/shared/input-token.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Input } from '@repo/ui/components/ui/input';
import { cn } from '@repo/ui/lib/utils';
import BalanceSelector from './balance-selector';
import BalanceSelector from './selectors/balance-selector';
import { Validation } from './validation-result';
import { InputBlock } from './input-block';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IconInput } from '@repo/ui/components/ui/icon-input';
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { Box } from '@repo/ui/components/ui/box';
import { motion } from 'framer-motion';
import { metadataBySearch } from './search-filters';

interface AssetSelectorProps {
assets: Metadata[];
Expand Down Expand Up @@ -53,7 +54,7 @@ const useFilteredAssets = ({ assets, value, onChange, filter }: AssetSelectorPro
const [search, setSearch] = useState('');

let filteredAssets = filter ? sortedAssets.filter(filter) : sortedAssets;
filteredAssets = search ? assets.filter(bySearch(search)) : assets;
filteredAssets = search ? assets.filter(metadataBySearch(search)) : assets;

useEffect(
() => switchAssetIfNecessary({ value, onChange, filter, assets: filteredAssets }),
Expand All @@ -63,10 +64,6 @@ const useFilteredAssets = ({ assets, value, onChange, filter }: AssetSelectorPro
return { filteredAssets, search, setSearch };
};

const bySearch = (search: string) => (asset: Metadata) =>
asset.display.toLocaleLowerCase().includes(search.toLocaleLowerCase()) ||
asset.symbol.toLocaleLowerCase().includes(search.toLocaleLowerCase());

/**
* Allows the user to select any asset known to Penumbra, optionally filtered by
* a filter function.
Expand Down Expand Up @@ -118,7 +115,7 @@ export const AssetSelector = ({ assets, onChange, value, filter }: AssetSelector

<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent layoutId={layoutId}>
<div className='flex max-h-screen flex-col'>
<div className='flex max-h-[90dvh] flex-col'>
<DialogHeader>Select asset</DialogHeader>

<div className='flex flex-col gap-2 overflow-auto p-4'>
Expand All @@ -127,6 +124,7 @@ export const AssetSelector = ({ assets, onChange, value, filter }: AssetSelector
icon={<MagnifyingGlassIcon className='size-5 text-muted-foreground' />}
value={search}
onChange={setSearch}
autoFocus
placeholder='Search assets...'
/>
</Box>
Expand Down
58 changes: 58 additions & 0 deletions apps/minifront/src/components/shared/selectors/balance-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { BalanceOrMetadata, isBalance, isMetadata } from './helpers';
import { getAddressIndex } from '@penumbra-zone/getters/address-view';
import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response';
import { useMemo } from 'react';
import { DialogClose } from '@repo/ui/components/ui/dialog';
import { cn } from '@repo/ui/lib/utils';
import { AssetIcon } from '@repo/ui/components/ui/tx/view/asset-icon';
import { ValueViewComponent } from '@repo/ui/components/ui/tx/view/value';

interface BalanceItemProps {
asset: BalanceOrMetadata;
value?: BalanceOrMetadata;
onSelect: (value: BalanceOrMetadata) => void;
}

export const BalanceItem = ({ asset, value, onSelect }: BalanceItemProps) => {
const account = isBalance(asset) ? getAddressIndex(asset.accountAddress).account : undefined;
const metadata = isMetadata(asset) ? asset : getMetadataFromBalancesResponseOptional(asset);

const isSelected = useMemo(() => {
if (!value) return false;
if (isMetadata(value) && isMetadata(asset)) {
return value.equals(asset);
}
if (isBalance(value) && isBalance(asset)) {
return value.equals(asset);
}
return false;
}, [asset, value]);

return (
<div className='flex flex-col'>
<DialogClose onClick={() => onSelect(asset)}>
<div
className={cn(
'grid grid-cols-5 py-[10px] gap-3 cursor-pointer hover:bg-light-brown hover:px-4 hover:-mx-4 font-bold text-muted-foreground',
isSelected && 'bg-light-brown px-4 -mx-4',
)}
>
{metadata && (
<div className='col-span-2 flex items-center justify-start gap-1'>
<AssetIcon metadata={metadata} />
<p className='truncate'>{metadata.symbol || 'Unknown asset'}</p>
</div>
)}

<div className='col-span-2 flex justify-end'>
{isBalance(asset) && (
<ValueViewComponent showIcon={false} showDenom={false} view={asset.balanceView} />
)}
</div>

<p className='flex justify-center'>{account}</p>
</div>
</DialogClose>
</div>
);
};
103 changes: 103 additions & 0 deletions apps/minifront/src/components/shared/selectors/balance-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
import { useId, useState } from 'react';
import { IconInput } from '@repo/ui/components/ui/icon-input';
import { Dialog, DialogContent, DialogHeader } from '@repo/ui/components/ui/dialog';
import { ValueViewComponent } from '@repo/ui/components/ui/tx/view/value';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { Box } from '@repo/ui/components/ui/box';
import { motion } from 'framer-motion';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { emptyBalanceResponse } from '../../../utils/empty-balance-response';
import { bySearch } from './search-filters';
import { BalanceOrMetadata, isMetadata, mergeBalancesAndAssets } from './helpers';
import { BalanceItem } from './balance-item';

interface BalanceSelectorProps {
value: BalancesResponse | undefined;
onChange: (selection: BalancesResponse) => void;
balances?: BalancesResponse[];
assets?: Metadata[];
}

/**
* Renders balances the user holds, and allows the user to select one. This is
* useful for a form where the user wants to send/sell/swap an asset that they
* already hold.
*
* Use `<AssetSelector />` if you want to render assets that aren't tied to any
* balance.
*/
export default function BalanceSelector({
value,
balances,
onChange,
assets,
}: BalanceSelectorProps) {
const [search, setSearch] = useState('');
const [isOpen, setIsOpen] = useState(false);
const layoutId = useId();

const allAssets = mergeBalancesAndAssets(balances, assets);
const filteredBalances = search ? allAssets.filter(bySearch(search)) : allAssets;

const onSelect = (asset: BalanceOrMetadata) => {
if (!isMetadata(asset)) {
onChange(asset);
return;
}
onChange(emptyBalanceResponse(asset));
};

return (
<>
{!isOpen && (
<motion.div
layout
layoutId={layoutId}
className='flex min-w-[100px] max-w-[200px] cursor-pointer items-center justify-center rounded-lg bg-light-brown px-2'
onClick={() => setIsOpen(true)}
>
<ValueViewComponent view={value?.balanceView} showValue={false} />
</motion.div>
)}

{isOpen && (
<>
{/* 0-opacity placeholder for layout's sake */}
<div className='flex min-w-[100px] max-w-[200px] px-2 opacity-0'>
<ValueViewComponent view={value?.balanceView} showValue={false} />
</div>
</>
)}

<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent layoutId={layoutId}>
<div className='flex max-h-[90dvh] flex-col'>
<DialogHeader>Select asset</DialogHeader>
<div className='flex shrink flex-col gap-4 overflow-auto p-4'>
<Box spacing='compact'>
<IconInput
icon={<MagnifyingGlassIcon className='size-5 text-muted-foreground' />}
value={search}
onChange={setSearch}
autoFocus
placeholder='Search assets...'
/>
</Box>
<div className='mt-2 grid grid-cols-4 gap-3 font-headline text-base font-semibold'>
<p className='col-span-2 flex justify-start'>Asset</p>
<p className='flex justify-end'>Balance</p>
<p className='flex justify-center'>Account</p>
</div>
<div className='flex flex-col gap-2'>
{filteredBalances.map((asset, i) => (
<BalanceItem key={i} asset={asset} value={value} onSelect={onSelect} />
))}
</div>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}
26 changes: 26 additions & 0 deletions apps/minifront/src/components/shared/selectors/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response';

export type BalanceOrMetadata = BalancesResponse | Metadata;

export const isMetadata = (asset: BalancesResponse | Metadata): asset is Metadata => {
return asset.getType().typeName === Metadata.typeName;
};

export const isBalance = (asset: BalancesResponse | Metadata): asset is BalancesResponse => {
return asset.getType().typeName === BalancesResponse.typeName;
};

export const mergeBalancesAndAssets = (
balances: BalancesResponse[] = [],
assets: Metadata[] = [],
): BalanceOrMetadata[] => {
const filteredAssets = assets.filter(asset => {
return !balances.some(balance => {
const balanceMetadata = getMetadataFromBalancesResponseOptional(balance);
return balanceMetadata?.equals(asset);
});
});
return [...balances, ...filteredAssets];
};
Loading

0 comments on commit 97b7231

Please sign in to comment.