Skip to content

Commit

Permalink
Feat: Swap Page UI (#65)
Browse files Browse the repository at this point in the history
* feat: swap page

* feat: mobile
  • Loading branch information
kamescg authored Dec 13, 2024
1 parent 7587f98 commit 480c261
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 75 deletions.
104 changes: 101 additions & 3 deletions apps/wallet/app/(site)/finance/swap/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,110 @@
'use client';
import { FormErc20Swap } from '@/components/forms/form-erc20-swap';
import { Badge } from '@/components/ui/badge';
import { AssetsListView } from '@/components/views/assets-list-view';
import { cn } from '@/lib/utils';
import { ArrowLeftFromLine, ArrowRightFromLine, Coins } from 'lucide-react';
import Image from 'next/image';
import { useState } from 'react';

export default function FinanceSwapPage() {
const [isMinimized, setIsMinimized] = useState(false);

return (
<>
<section className="flex h-full flex-col bg-neutral-100 md:items-center md:justify-center">
<div className="mx-auto w-full max-w-screen-sm p-4">
<FormErc20Swap />
<section className="flex h-full flex-col bg-gradient-to-br bg-neutral-100 from-neutral-50 to-stone-200">
<div className="mx-auto grid h-full w-full gap-x-4 p-0 md:grid-cols-12">
<div
className={cn(
'order-2 hidden space-y-2 bg-white px-2 py-2 shadow-md transition-shadow hover:shadow-xl md:order-1 md:block md:py-4',
{
'md:col-span-1 md:px-2.5': isMinimized,
'col-span-1 md:col-span-3 md:px-4': !isMinimized,
},
)}
>
<div className="flex h-full w-full flex-col space-y-2">
<div className="flex items-center justify-between border-neutral-100 border-b-2 pb-2">
{isMinimized ? (
<Coins className="size-3 text-muted-foreground" />
) : (
<h3 className="font-semibold text-muted-foreground text-xs">
Assets
</h3>
)}
{isMinimized ? (
<Badge
variant={'outline'}
className="cursor-pointer transition-shadow hover:shadow-md"
onClick={() => setIsMinimized(false)}
>
<ArrowRightFromLine
className="size-3 cursor-pointer"
size={15}
onClick={() => setIsMinimized(false)}
/>
</Badge>
) : (
<Badge
variant={'outline'}
className="cursor-pointer transition-shadow hover:shadow-md"
onClick={() => setIsMinimized(true)}
>
<ArrowLeftFromLine className="size-3" size={15} />
</Badge>
)}
</div>
<AssetsListView className="flex-1" isMinimized={isMinimized} />
<div
className={cn(
'flex items-center justify-between border-t-2 pt-3',
{
'flex-col gap-y-1': isMinimized,
'flex-row': !isMinimized,
},
)}
>
<span
className={cn({
'text-xs': isMinimized,
'text-sm': !isMinimized,
})}
>
Credit
</span>
<div className="flex items-center gap-x-1">
<Image
alt="USDC"
className={cn({
'size-3': isMinimized,
'size-4': !isMinimized,
})}
src="https://ethereum-optimism.github.io/data/USDC/logo.png"
width={20}
height={20}
/>
<span
className={cn('font-semibold', {
'text-xs': isMinimized,
'text-sm': !isMinimized,
})}
>
0.00
</span>
</div>
</div>
</div>
</div>
<div
className={cn('flex flex-col justify-center md:order-2 md:px-20', {
'md:col-span-11': isMinimized,
'md:col-span-9': !isMinimized,
})}
>
<div className="container max-w-screen-sm">
<FormErc20Swap />
</div>
</div>
</div>
</section>
</>
Expand Down
5 changes: 3 additions & 2 deletions apps/wallet/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@
@apply mb-3;
}

.content a {
@apply text-blue-600 dark:text-blue-300 font-bold;
.content a,
.link {
@apply text-blue-600 hover:text-blue-700 dark:text-blue-300;
}

.smart-wallet-connect {
Expand Down
10 changes: 7 additions & 3 deletions apps/wallet/src/components/fields/erc20-select-and-amount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface Erc20SelectAndAmount {
amountName?: string;
tokenName?: string;
disabled?: boolean;
amountDisabled?: boolean;
tokenSelectorDisabled?: boolean;
setMaxAmount?: (value: string) => void;
}

Expand All @@ -23,6 +25,8 @@ export function Erc20SelectAndAmount({
label = 'Asset',
amountName = 'amount',
tokenName = 'token',
amountDisabled,
tokenSelectorDisabled,
setMaxAmount,
}: Erc20SelectAndAmount) {
const { control } = useFormContext();
Expand All @@ -38,7 +42,7 @@ export function Erc20SelectAndAmount({
<FormItem>
<FormControl>
<Input
disabled={disabled}
disabled={disabled || amountDisabled}
id="amount"
className="block h-auto w-full flex-1 border-transparent bg-transparent py-1 pl-0 text-left font-bold text-5xl shadow-none placeholder:text-muted-foreground focus:border-transparent focus:ring-transparent focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-transparent"
placeholder="0.0"
Expand All @@ -52,11 +56,11 @@ export function Erc20SelectAndAmount({
<FormField
control={control}
name={tokenName}
render={({ field, ...rest }) => (
render={({ field }) => (
<FormItem className="flex flex-col justify-end">
<FormControl>
<TokenSelector
disabled={disabled}
disabled={disabled || tokenSelectorDisabled}
tokenList={tokenList}
value={field.value}
onValueChange={field.onChange}
Expand Down
13 changes: 12 additions & 1 deletion apps/wallet/src/components/forms/form-erc20-swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Card } from '@/components/ui/card';
import { Form } from '@/components/ui/form';
import { defaultTokenList, useIsValidChain } from '@/lib/chains';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { ANY_DELEGATE, findToken, getDefaultTokenList } from 'universal-data';
import { useSignErc20SwapDelegation } from 'universal-delegations-sdk';
Expand Down Expand Up @@ -44,7 +45,6 @@ function FormErc20Swap({ defaultValues }: FormErc20SwapProps) {
const { isValidChain, chainId, defaultChain } = useIsValidChain();

const tokenList = getDefaultTokenList({ chainId });

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
Expand All @@ -59,6 +59,16 @@ function FormErc20Swap({ defaultValues }: FormErc20SwapProps) {
},
});

const formValues = form.watch();

useEffect(() => {
if (formValues.tokenOut && formValues.tokenIn && formValues.amountOut) {
form.setValue('amountIn', formValues.amountOut);
} else {
form.setValue('amountIn', undefined);
}
}, [form, formValues.amountOut, formValues.tokenOut, formValues.tokenIn]);

async function onSubmit(data: z.infer<typeof formSchema>) {
if (!isValidChain) {
switchChain({
Expand Down Expand Up @@ -103,6 +113,7 @@ function FormErc20Swap({ defaultValues }: FormErc20SwapProps) {
amountName="amountIn"
tokenName="tokenIn"
tokenList={defaultTokenList}
amountDisabled={true}
/>
</Card>
{address && isValidChain && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LinkComponent } from '../ui/link-component';

interface AccountExplorerLinkAddressProps
extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
address?: AddressType;
address?: AddressType | string;
chain?: Chain;
}

Expand Down
31 changes: 31 additions & 0 deletions apps/wallet/src/components/onchain/etherscan-address-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defaultChain } from '@/lib/chains';
import type { Address as AddressType, Chain } from 'viem';
import { LinkComponent } from '../ui/link-component';

type ExplorerAddressLinkProps = React.HTMLAttributes<HTMLElement> & {
address?: AddressType | string;
chain?: Chain;
};

export const ExplorerAddressLink = ({
children,
address,
className,
chain = defaultChain,
}: ExplorerAddressLinkProps) => {
const blockExplorerUrl = chain.blockExplorers?.default.url;

if (blockExplorerUrl) {
return (
<LinkComponent
isExternal={true}
className={className}
href={`${blockExplorerUrl}/address/${address}`}
>
{children}
</LinkComponent>
);
}

return null;
};
134 changes: 134 additions & 0 deletions apps/wallet/src/components/views/assets-list-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { cn } from '@/lib/utils';
import type * as React from 'react';

import { defaultTokenList } from '@/lib/chains';
import Image from 'next/image';
import type { TokenItem } from 'universal-types';
import type { Address } from 'viem';
import { ERC20Balance } from '../onchain/erc20-balance';
import { ExplorerAddressLink } from '../onchain/etherscan-address-link';
import { LinkComponent } from '../ui/link-component';

type AssetsListView = React.HTMLAttributes<HTMLElement> & {
isMinimized?: boolean;
};

const AssetsListView = ({ className, isMinimized }: AssetsListView) => {
return (
<div className={cn('space-y-0', className)}>
{defaultTokenList.tokens.map((token) => {
return (
<AssetCard
token={token}
key={token.address}
isMinimized={isMinimized}
/>
);
})}
</div>
);
};

export { AssetsListView };

type AssetCard = React.HTMLAttributes<HTMLElement> & {
token: TokenItem;
isMinimized?: boolean;
};

const AssetCard = ({ className, token, isMinimized }: AssetCard) => {
return (
<Dialog>
<DialogTrigger asChild={true}>
{isMinimized ? (
<div className="flex cursor-pointer items-center justify-between gap-x-1 rounded-xl px-1 py-3 transition-shadow hover:bg-neutral-50 hover:shadow-sm">
<ERC20Balance
address={token.address as Address}
decimals={token.decimals}
className="font-semibold text-xs"
/>
<Image
src={token.logoURI}
alt={token.name}
width={32}
height={32}
className="size-4 rounded-full shadow-md"
/>
</div>
) : (
<div
className={cn(
'flex cursor-pointer items-center justify-between rounded-xl p-3 transition-shadow hover:bg-neutral-50 hover:shadow-sm',
className,
)}
>
<div className="flex items-center gap-x-2">
<Image
src={token.logoURI}
alt={token.name}
width={32}
height={32}
className="size-6 rounded-full shadow-md"
/>
<div className="flex flex-col">
<span className="font-bold text-sm">
{token.symbol}{' '}
{token?.extensions?.metadata?.type ? (
<span className="font-mono font-normal text-2xs">
({token?.extensions?.metadata?.type})
</span>
) : null}
</span>
<span className="text-2xs text-gray-500">{token.name}</span>
</div>
</div>
<ERC20Balance
address={token.address as Address}
decimals={token.decimals}
className="font-semibold text-sm"
/>
</div>
)}
</DialogTrigger>
<DialogContent className="w-full md:max-w-lg">
<DialogHeader>
<Image
src={token.logoURI}
alt={token.name}
width={32}
height={32}
className="size-10 rounded-full shadow-md"
/>
<DialogTitle className="font-black text-xl">
{token.name} <span className="font-normal">({token.symbol})</span>
</DialogTitle>
<div className="flex items-center gap-x-1 text-sm">
<LinkComponent
className="link"
href={token?.extensions?.metadata?.url}
>
Website
</LinkComponent>
|
<ExplorerAddressLink className="link" address={token.address}>
Explorer
</ExplorerAddressLink>
</div>
<hr className="my-6 inline-block" />
<div className="content py-1 text-muted-foreground text-sm leading-5">
{token?.extensions?.metadata?.description}
</div>
<hr className="my-6 inline-block" />
<span className="text-xs">{token.address}</span>
</DialogHeader>
</DialogContent>
</Dialog>
);
};
Loading

0 comments on commit 480c261

Please sign in to comment.