Skip to content

Commit

Permalink
feat: new ui
Browse files Browse the repository at this point in the history
  • Loading branch information
aon committed Dec 6, 2024
1 parent d8463a9 commit 96a144d
Show file tree
Hide file tree
Showing 23 changed files with 1,883 additions and 663 deletions.
2 changes: 1 addition & 1 deletion web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => {
return (
<html suppressHydrationWarning className={`${GeistSans.variable} ${GeistMono.variable}`}>
<body>
<ThemeProvider forcedTheme="light">
<ThemeProvider forcedTheme="dark" attribute="class">
<ScaffoldEthAppWithProviders>{children}</ScaffoldEthAppWithProviders>
</ThemeProvider>
</body>
Expand Down
255 changes: 144 additions & 111 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
"use client";

import React, { useEffect, useMemo, useState } from "react";
import React, { useMemo, useState } from "react";
import Image from "next/image";
import toast from "react-hot-toast";
import { useBoolean } from "usehooks-ts";
import { parseUnits } from "viem";
import { formatEther } from "viem";
import { useAccount, useWriteContract } from "wagmi";
import { ArrowsUpDownIcon, LockClosedIcon } from "@heroicons/react/24/outline";
import { ArrowsUpDownIcon, LockClosedIcon, WalletIcon } from "@heroicons/react/24/outline";
import { Button } from "~~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~~/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~~/components/ui/select";
import { CPAMM_ABI, CPAMM_ADDRESS } from "~~/contracts/cpamm";
import { DAI_TOKEN, ERC20_ABI, WBTC_TOKEN } from "~~/contracts/tokens";
import { DAI_TOKEN, ERC20_ABI, Token, WBTC_TOKEN } from "~~/contracts/tokens";
import useCpamm from "~~/hooks/use-cpamm";
import useDaiToken from "~~/hooks/use-dai-token";
import useWbtcToken from "~~/hooks/use-wbtc-token";
import { cn } from "~~/utils/cn";
import { formatTokenWithDecimals } from "~~/utils/currency";
import waitForTransactionReceipt from "~~/utils/wait-for-transaction";

interface SwapState {
from: {
token: Token;
amount: bigint;
displayAmount: string;
};
to: {
token: Token;
amount: bigint;
displayAmount: string;
};
}

export default function UniswapClone() {
const dai = useDaiToken();
const wbtc = useWbtcToken();
const { writeContractAsync } = useWriteContract();
const { daiPoolLiquidity, wbtcPoolLiquidity, refetchAll } = useCpamm();
const { daiPoolLiquidity, wbtcPoolLiquidity } = useCpamm();
const { isConnected } = useAccount();
const { value: isSwapping, setValue: setIsSwapping } = useBoolean(false);
const [swapState, setSwapState] = useState({
const [swapState, setSwapState] = useState<SwapState>({
from: {
token: DAI_TOKEN,
amount: 0n,
Expand All @@ -36,13 +52,6 @@ export default function UniswapClone() {
},
});

useEffect(() => {
const interval = setInterval(() => {
refetchAll();
}, 5000);
return () => clearInterval(interval);
}, [refetchAll]);

const fromBalance = swapState.from.token.symbol === "DAI" ? dai.balance : wbtc.balance;
const toBalance = swapState.to.token.symbol === "DAI" ? dai.balance : wbtc.balance;

Expand Down Expand Up @@ -84,7 +93,13 @@ export default function UniswapClone() {
}

try {
const newAmount = parseUnits(newDisplayAmount, 18);
let newAmount: bigint;
try {
newAmount = parseUnits(newDisplayAmount, 18);
} catch {
// Invalid input, do nothing
return;
}
const newAmountWithFee = (newAmount * 997n) / 1000n; // 0.3% fee

const oppositeAmount = (() => {
Expand Down Expand Up @@ -112,16 +127,16 @@ export default function UniswapClone() {
[oppositeTokenType]: {
...prev[oppositeTokenType],
amount: oppositeAmount,
displayAmount: formatEther(oppositeAmount),
displayAmount: formatTokenWithDecimals(oppositeAmount, 18),
},
}));
} catch (error) {
console.error("Error calculating amounts:", error);
}
};

const handleTokenChange = (e: React.ChangeEvent<HTMLSelectElement>, tokenType: "from" | "to") => {
const newToken = e.target.value === "DAI" ? DAI_TOKEN : WBTC_TOKEN;
const handleTokenChange = (value: string, tokenType: "from" | "to") => {
const newToken = value === "DAI" ? DAI_TOKEN : WBTC_TOKEN;
if (swapState[tokenType].token.symbol === newToken.symbol) {
return;
}
Expand All @@ -148,7 +163,7 @@ export default function UniswapClone() {
};

const handleSwap = async () => {
if (!swapState.from.amount || !swapState.to.amount || !fromAllowance) {
if (!swapState.from.amount || !swapState.to.amount || fromAllowance === undefined) {
return;
}

Expand Down Expand Up @@ -228,105 +243,59 @@ export default function UniswapClone() {

return (
<div className="flex-1 flex items-center justify-center relative">
<div className="card min-w-[450px] shadow-lg bg-white">
<form
className={cn("card-body", !isConnected && "opacity-60")}
onSubmit={e => {
e.preventDefault();
handleSwap();
}}
>
<h2 className="card-title mb-4 font-medium text-2xl">Swap Tokens</h2>
<div className="form-control gap-2">
<label className="label">
<span className="label-text text-lg">From</span>
<span className="label-text-alt">
Balance: {formatTokenWithDecimals(fromBalance ?? 0n, 18)} {swapState.from.token.symbol}
</span>
</label>
<select
className="select select-bordered"
value={swapState.from.token.symbol}
onChange={e => handleTokenChange(e, "from")}
disabled={isSwapping}
>
<option key="DAI" value="DAI">
{DAI_TOKEN.name} ({DAI_TOKEN.symbol})
</option>
<option key="WBTC" value="WBTC">
{WBTC_TOKEN.name} ({WBTC_TOKEN.symbol})
</option>
</select>
<input
type="number"
step="any"
placeholder="0"
className="input input-bordered"
min={0}
value={swapState.from.displayAmount}
onChange={e => handleAmountChange(e, "from")}
disabled={isSwapping}
/>
</div>
<form
className={cn(!isConnected && "opacity-60")}
onSubmit={e => {
e.preventDefault();
handleSwap();
}}
>
<h2 className="card-title mb-4 font-medium text-2xl">Swap</h2>

<div className="flex justify-center my-2">
<button className="btn btn-outline" onClick={handleSwitch} disabled={isSwapping} type="button">
<ArrowsUpDownIcon className="h-4 w-4" />
</button>
</div>
<SwapCard
heading="Sell"
balance={fromBalance ?? 0n}
displayAmount={swapState.from.displayAmount}
token={swapState.from.token}
selectedToken={swapState.from.token.symbol}
onAmountChange={e => handleAmountChange(e, "from")}
onTokenChange={e => handleTokenChange(e, "from")}
disabled={isSwapping}
/>

<div className="form-control gap-2">
<label className="label">
<span className="label-text text-lg">To</span>
<span className="label-text-alt">
Balance: {formatTokenWithDecimals(toBalance ?? 0n, 18)} {swapState.to.token.symbol}
</span>
</label>
<select
className="select select-bordered"
value={swapState.to.token.symbol}
onChange={e => handleTokenChange(e, "to")}
disabled={isSwapping}
>
<option key="DAI" value="DAI">
{DAI_TOKEN.name} ({DAI_TOKEN.symbol})
</option>
<option key="WBTC" value="WBTC">
{WBTC_TOKEN.name} ({WBTC_TOKEN.symbol})
</option>
</select>
<input
type="number"
step="any"
placeholder="0"
className="input input-bordered"
value={swapState.to.displayAmount}
onChange={e => handleAmountChange(e, "to")}
disabled={isSwapping}
/>
</div>
<div className="flex justify-center -my-3">
<Button variant="secondary" className="h-10 w-10" onClick={handleSwitch} disabled={isSwapping} type="button">
<ArrowsUpDownIcon className="h-5 w-5" />
</Button>
</div>

<div className="mt-4 text-sm">
<p>
Swap Price: 1 {swapState.from.token.symbol} = {formatTokenWithDecimals(swapPrice, 18)}{" "}
{swapState.to.token.symbol}
</p>
</div>
<SwapCard
heading="Buy"
balance={toBalance ?? 0n}
displayAmount={swapState.to.displayAmount}
token={swapState.to.token}
selectedToken={swapState.to.token.symbol}
onAmountChange={e => handleAmountChange(e, "to")}
onTokenChange={e => handleTokenChange(e, "to")}
disabled={isSwapping}
/>

<div className="card-actions justify-end mt-6">
<button
className="btn btn-primary"
type="submit"
disabled={!swapState.from.amount || !swapState.to.amount || isSwapping}
>
Swap
</button>
</div>
</form>
</div>
<div className="mt-4 text-sm text-muted-foreground">
Swap Price: 1 {swapState.from.token.symbol} = {formatTokenWithDecimals(swapPrice, 18)}{" "}
{swapState.to.token.symbol}
</div>

<Button
className="w-full h-11 mt-4 text-lg"
type="submit"
disabled={!swapState.from.amount || !swapState.to.amount || isSwapping}
>
Swap
</Button>
</form>
{!isConnected && (
<div className="absolute inset-0 flex items-center justify-center backdrop-blur-sm rounded-lg">
<div className="rounded-lg bg-neutral-800 border px-6 py-4 text-lg font-medium text-neutral-50 shadow-lg flex items-center gap-x-2">
<div className="rounded-lg bg-zinc-900 border px-6 py-4 text-lg font-medium text-neutral-50 shadow-lg flex items-center gap-x-2">
<LockClosedIcon className="w-5 h-5" />
Connect your wallet to start
</div>
Expand All @@ -335,3 +304,67 @@ export default function UniswapClone() {
</div>
);
}

function SwapCard({
heading,
balance,
onAmountChange,
onTokenChange,
disabled,
displayAmount,
token,
selectedToken,
}: {
heading: string;
balance: bigint;
displayAmount: string;
token: Token;
onAmountChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
selectedToken: Token["symbol"];
onTokenChange: (value: string) => void;
disabled: boolean;
}) {
return (
<Card>
<CardHeader className="p-4 pb-3">
<CardTitle className="text-muted-foreground text-sm font-normal">{heading}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-y-2 p-4 pt-0">
<div className="flex items-center justify-between">
<input
placeholder="0"
className="bg-transparent appearance-none focus:outline-none text-3xl"
value={displayAmount}
onChange={onAmountChange}
disabled={disabled}
/>
<div className="flex flex-col gap-y-2">
<Select value={selectedToken} disabled={disabled} onValueChange={onTokenChange}>
<SelectTrigger className="bg-secondary text-secondary-foreground shadow hover:bg-secondary/80 text-base h-fit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="DAI">
<div className="flex items-center gap-x-2 mr-2">
<Image src={DAI_TOKEN.logo} alt={DAI_TOKEN.symbol} width={20} height={20} />
{DAI_TOKEN.symbol}
</div>
</SelectItem>
<SelectItem value="WBTC">
<div className="flex items-center gap-x-2 mr-2">
<Image src={WBTC_TOKEN.logo} alt={WBTC_TOKEN.symbol} width={20} height={20} />
{WBTC_TOKEN.symbol}
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-x-2 text-sm text-muted-foreground self-end">
<WalletIcon className="h-4 w-4" />
{formatTokenWithDecimals(balance, token.decimals)}
</div>
</CardContent>
</Card>
);
}
Loading

0 comments on commit 96a144d

Please sign in to comment.