Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/web-app/public/images/bob.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions apps/web-app/public/images/btc.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
185 changes: 185 additions & 0 deletions apps/web-app/src/components/Convert/Convert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useNavigate, useSearch } from '@tanstack/react-router';
import { ArrowDownUp, Info, Settings } from 'lucide-react';
import { useState, type FC } from 'react';
import { Hero } from '../Hero/Hero';
import { AssetSelect } from './components/AssetSelect';
import { CryptoChart } from './components/ConvertChart';

export const Convert: FC = () => {
const search = useSearch({ from: '/convert' });
const navigate = useNavigate({ from: '/convert' });

const [fromAsset, setFromAsset] = useState('BTC');
const [toAsset, setToAsset] = useState('BTC');
Comment on lines +18 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a constant for BTC.

const [amount, setAmount] = useState('1');

const showChart = search.showChart === true;

const toggleChart = () => {
navigate({
search: (prev) => ({
...prev,
showChart: prev.showChart ? undefined : true,
}),
replace: true,
});
};

const handleSwap = () => {
setFromAsset(toAsset);
setToAsset(fromAsset);
};

return (
<div>
<div className="flex flex-col lg:flex-row gap-6 justify-center">
<Hero className="w-full lg:w-1/3" title="Convert">
Lorem bitcoinae dollar situs ametus, consensusium adipiscing elitum,
sed do proofus-of-workium.
</Hero>
{showChart && <div className="w-full lg:w-1/2" />}
</div>

<div className="flex flex-col lg:flex-row items-center lg:items-start gap-6 justify-center">
<div className="flex flex-col gap-6 w-full lg:w-1/3">
<Card className="bg-neutral-900 border-none rounded-2xl shadow-lg py-3">
<CardContent className="space-y-4 px-3">
<div className="flex items-center justify-between">
<Tabs defaultValue="convert" className="flex-1">
<TabsList className="flex items-center bg-neutral-800 rounded-full p-1 w-full max-w-md mx-auto">
<TabsTrigger value="convert">Convert</TabsTrigger>
<TabsTrigger value="cross">Cross-chain convert</TabsTrigger>
</TabsList>
</Tabs>
<Button
variant="ghost"
size="icon"
className="ml-2 text-neutral-400 hover:text-white cursor-pointer"
>
<Settings className="size-6" />
</Button>
</div>

<div className="space-y-3">
<div className="p-4 bg-neutral-800 rounded-xl flex flex-col gap-4">
<Label className="text-neutral-400 font-medium text-sm">
From
</Label>
<div className="flex justify-between items-center">
<AssetSelect value={fromAsset} onChange={setFromAsset} />
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0"
className="flex-1 bg-transparent border-none text-right text-2xl font-semibold focus-visible:ring-0 text-white outline-none
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>
<div className="flex justify-between items-center text-neutral-400 font-medium text-xs">
<span>Balance -</span>
<span>$100.012</span>
</div>
</div>

<div className="flex justify-center">
<Button
className="bg-accent"
variant="ghost"
size="icon"
onClick={handleSwap}
>
<ArrowDownUp className="h-5 w-5 text-neutral-400" />
</Button>
</div>

<div className="p-4 bg-neutral-800 rounded-xl flex flex-col gap-4">
<Label className="text-neutral-400 font-medium text-sm">
From
</Label>
<div className="flex justify-between items-center">
<AssetSelect value={fromAsset} onChange={setFromAsset} />
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0"
className="flex-1 bg-transparent border-none text-right text-2xl font-semibold focus-visible:ring-0 text-white outline-none
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>
<div className="flex justify-between items-center text-neutral-400 font-medium text-xs">
<span>Balance -</span>
<span>$100.012</span>
</div>
</div>

<div className="flex justify-end items-center gap-2">
<Label
htmlFor="chart"
className="text-sm cursor-pointer text-gray-50 font-medium"
>
Show chart
</Label>
<Switch
className="cursor-pointer"
checked={showChart}
id="chart"
onCheckedChange={toggleChart}
/>
</div>
</div>
</CardContent>
</Card>
<div className="rounded-xl border border-neutral-700 p-4 space-y-2 text-xs font-medium text-gray-400">
<div className="flex justify-between">
<span>You’ll receive</span>
<span>0.004 XYZ</span>
</div>
<div className="flex justify-between items-center">
<span className="flex items-center gap-1">
Estimated network fee <Info className="w-3 h-3" />
</span>
<span>0.004 XYZ</span>
</div>
<div className="flex justify-between">
<span>Transaction fee</span>
<span>0.004 XYZ</span>
</div>
</div>

<div className="space-y-1 mx-auto">
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<Label
htmlFor="terms"
className="text-sm font-medium text-gray-50"
>
Accept terms and condition
</Label>
</div>
<p className="text-sm text-gray-50 pl-6">
You agree to our Terms of Service and Privacy Policy.
</p>
</div>

<Button size="lg" className="w-40 mx-auto">
Convert
</Button>
</div>

{showChart && (
<div className="w-full lg:w-1/2">
<CryptoChart />
</div>
)}
</div>
</div>
);
};
38 changes: 38 additions & 0 deletions apps/web-app/src/components/Convert/components/AssetSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { BOB, BTC, USDT } from '@/constants/tokens';
import type { FC } from 'react';

interface AssetSelectProps {
value?: string;
onChange?(value: string): void;
}

export const AssetSelect: FC<AssetSelectProps> = ({ value, onChange }) => {
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="shadow-none min-w-36 border-none text-lg cursor-pointer h-5 pl-0">
<SelectValue placeholder="Token" />
</SelectTrigger>
<SelectContent>
<SelectItem className="text-lg" value={BTC}>
<img className="size-8 rounded-full" src="/images/btc.svg" />
{BTC}
</SelectItem>
<SelectItem className="text-lg" value={BOB}>
<img className="size-8 rounded-full" src="/images/bob.svg" />
{BOB}
</SelectItem>
<SelectItem className="text-lg" value={USDT}>
<img className="size-8 rounded-full" src="/images/btc.svg" />
{USDT}
</SelectItem>
</SelectContent>
</Select>
);
};
140 changes: 140 additions & 0 deletions apps/web-app/src/components/Convert/components/ConvertChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use client';

import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { useState, type FC } from 'react';
import {
Area,
AreaChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';

const data = [
{ date: 'Sep 16', value: 620000 },
{ date: 'Sep 17', value: 580000 },
{ date: 'Sep 18', value: 610000 },
{ date: 'Sep 19', value: 480000 },
{ date: 'Sep 20', value: 450000 },
{ date: 'Sep 21', value: 200000 },
{ date: 'Sep 22', value: 300000 },
{ date: 'Sep 23', value: 465123 },
];

export const CryptoChart: FC = () => {
const [activeRange, setActiveRange] = useState('1W');
const ranges = ['24H', '1W', '1M', '1Y'];

return (
<Card className="bg-neutral-900 border-0 rounded-2xl pt-4 pb-7">
<CardContent className="px-7">
<div className="flex justify-between items-start mb-2">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="flex items-center">
<img className="size-8 rounded-full" src="/images/btc.svg" />
<img
className="size-8 rounded-full -ml-2"
src="/images/bob.svg"
/>
</div>
<span className="font-medium text-lg text-gray-50">
BTC / BOS
</span>
</div>
<div className="text-lg font-semibold text-gray-50 pl-1">
1,234,567.12
</div>
<div className="text-sm font-medium text-gray-500 pl-1">
Sept 23, 2025
</div>
</div>

<div className="flex gap-1">
{ranges.map((range) => (
<Button
key={range}
size="sm"
variant="ghost"
className={cn(
'cursor-pointer px-3 py-1.5 text-xs transition-colors text-white rounded-sm',
{
'bg-neutral-700': activeRange === range,
},
)}
onClick={() => setActiveRange(range)}
>
{range}
</Button>
))}
</div>
</div>

<div className="h-[320px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data}>
<defs>
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FF4500" stopOpacity={0.8} />
<stop offset="100%" stopColor="#FF4500" stopOpacity={0} />
</linearGradient>
</defs>

<XAxis
dataKey="date"
stroke="none"
tick={{ fill: '#6B7280', fontSize: 12, textAnchor: 'start' }}
tickLine={false}
axisLine={false}
interval={0}
padding={{ left: 10, right: 10 }}
/>

<YAxis
orientation="right"
stroke="none"
tick={{ fill: '#6B7280', fontSize: 12 }}
tickLine={false}
axisLine={false}
width={60}
tickFormatter={(v) =>
v === 0
? ''
: v.toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})
}
padding={{ top: 10, bottom: 10 }}
/>

<Area
type="monotone"
dataKey="value"
stroke="#FF4500"
strokeWidth={2}
fill="url(#colorValue)"
dot={false}
/>

<Tooltip
contentStyle={{
backgroundColor: '#1a1a1a',
border: 'none',
borderRadius: '4px',
}}
labelStyle={{ color: '#999' }}
formatter={(value) =>
value.toLocaleString('en-US', { minimumFractionDigits: 2 })
}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
};
Loading