Skip to content

Commit 6c0893a

Browse files
committed
refactor: data table
1 parent eefebc5 commit 6c0893a

8 files changed

Lines changed: 372 additions & 303 deletions

File tree

app/coins/[id]/page.tsx

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,7 @@ import {
77
import { Converter } from '@/components/coin-details/Converter';
88
import LiveDataWrapper from '@/components/LiveDataWrapper';
99
import { TopGainersLosers } from '@/components/coin-details/TopGainersLosers';
10-
import {
11-
Table,
12-
TableBody,
13-
TableCell,
14-
TableHead,
15-
TableHeader,
16-
TableRow,
17-
} from '@/components/ui/table';
10+
import { DataTable } from '@/components/DataTable';
1811
import { formatPrice, timeAgo } from '@/lib/utils';
1912
import Link from 'next/link';
2013
import { ArrowUpRight } from 'lucide-react';
@@ -62,6 +55,44 @@ const CoinDetails = async ({ params }: { params: Promise<{ id: string }> }) => {
6255
},
6356
];
6457

58+
const exchangeColumns = [
59+
{
60+
header: 'Exchange',
61+
cellClassName: ' text-green-500 font-bold',
62+
cell: (ticker: Ticker) => (
63+
<Link
64+
href={ticker.trade_url}
65+
target='_blank'
66+
className='exchange-link'
67+
>
68+
{ticker.market.name}
69+
</Link>
70+
),
71+
},
72+
{
73+
header: 'Pair',
74+
cell: (ticker: Ticker) => (
75+
<div className='exchange-pair'>
76+
<p className='truncate max-w-[100px] h-full'>{ticker.base}</p>
77+
/
78+
<p className='truncate max-w-[100px] h-full ml-2'>
79+
{ticker.target}
80+
</p>
81+
</div>
82+
),
83+
},
84+
{
85+
header: 'Price',
86+
cellClassName: 'font-medium',
87+
cell: (ticker: Ticker) => formatPrice(ticker.converted_last.usd),
88+
},
89+
{
90+
header: 'Last Traded',
91+
cellClassName: 'exchange-timestamp',
92+
cell: (ticker: Ticker) => timeAgo(ticker.timestamp),
93+
},
94+
];
95+
6596
return (
6697
<main className='coin-details-main'>
6798
<section className='size-full xl:col-span-2'>
@@ -74,57 +105,14 @@ const CoinDetails = async ({ params }: { params: Promise<{ id: string }> }) => {
74105
<div className='w-full mt-8 space-y-4'>
75106
<h4 className='section-title'>Exchange Listings</h4>
76107
<div className='custom-scrollbar mt-5 exchange-container'>
77-
<Table>
78-
<TableHeader className='text-purple-100'>
79-
<TableRow className='hover:bg-transparent'>
80-
<TableHead className='exchange-header-left'>
81-
Exchange
82-
</TableHead>
83-
<TableHead className='text-purple-100'>Pair</TableHead>
84-
<TableHead className='text-purple-100'>Price</TableHead>
85-
<TableHead className='exchange-header-right'>
86-
Last Traded
87-
</TableHead>
88-
</TableRow>
89-
</TableHeader>
90-
<TableBody>
91-
{coinData.tickers
92-
.slice(0, 7)
93-
.map((ticker: Ticker, index: number) => (
94-
<TableRow
95-
key={index}
96-
className='overflow-hidden rounded-lg hover:bg-dark-400/30!'
97-
>
98-
<TableCell className=' text-green-500 font-bold'>
99-
<Link
100-
href={ticker.trade_url}
101-
target='_blank'
102-
className='exchange-link'
103-
>
104-
{ticker.market.name}
105-
</Link>
106-
</TableCell>
107-
<TableCell>
108-
<div className='exchange-pair'>
109-
<p className='truncate max-w-[100px] h-full'>
110-
{ticker.base}
111-
</p>
112-
/
113-
<p className='truncate max-w-[100px] h-full ml-2'>
114-
{ticker.target}
115-
</p>
116-
</div>
117-
</TableCell>
118-
<TableCell className='font-medium'>
119-
{formatPrice(ticker.converted_last.usd)}
120-
</TableCell>
121-
<TableCell className='exchange-timestamp'>
122-
{timeAgo(ticker.timestamp)}
123-
</TableCell>
124-
</TableRow>
125-
))}
126-
</TableBody>
127-
</Table>
108+
<DataTable
109+
columns={exchangeColumns}
110+
data={coinData.tickers.slice(0, 7)}
111+
rowKey={(_, index) => index}
112+
headerClassName='text-purple-100'
113+
headerRowClassName='hover:bg-transparent'
114+
bodyRowClassName='overflow-hidden rounded-lg hover:bg-dark-400/30!'
115+
/>
128116
</div>
129117
</div>
130118
</LiveDataWrapper>

app/coins/page.tsx

Lines changed: 67 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import { getCoinList } from '@/lib/coingecko.actions';
2-
import {
3-
Table,
4-
TableBody,
5-
TableCell,
6-
TableHead,
7-
TableHeader,
8-
TableRow,
9-
} from '@/components/ui/table';
2+
import { DataTable } from '@/components/DataTable';
103
import Image from 'next/image';
114
import Link from 'next/link';
125

@@ -33,70 +26,77 @@ const Coins = async ({
3326
const estimatedTotalPages =
3427
currentPage >= 100 ? Math.ceil(currentPage / 100) * 100 + 100 : 100;
3528

29+
const columns = [
30+
{
31+
header: 'Rank',
32+
cellClassName: 'coins-rank',
33+
cell: (coin: CoinMarketData) => (
34+
<>
35+
#{coin.market_cap_rank}
36+
<Link
37+
href={`/coins/${coin.id}`}
38+
className='absolute inset-0 z-10'
39+
aria-label='View coin'
40+
/>
41+
</>
42+
),
43+
},
44+
{
45+
header: 'Token',
46+
cellClassName: 'coins-token',
47+
cell: (coin: CoinMarketData) => (
48+
<div className='coins-token-info'>
49+
<Image src={coin.image} alt={coin.name} width={36} height={36} />
50+
<p className='max-w-full truncate'>
51+
{coin.name} ({coin.symbol.toUpperCase()})
52+
</p>
53+
</div>
54+
),
55+
},
56+
{
57+
header: 'Price',
58+
cellClassName: 'coins-price',
59+
cell: (coin: CoinMarketData) => formatPrice(coin.current_price),
60+
},
61+
{
62+
header: '24h Change',
63+
cellClassName: 'font-medium',
64+
cell: (coin: CoinMarketData) => {
65+
const isTrendingUp = coin.price_change_percentage_24h > 0;
66+
67+
return (
68+
<span
69+
className={cn('coins-change', {
70+
'text-green-600': isTrendingUp,
71+
'text-red-500': !isTrendingUp,
72+
})}
73+
>
74+
{isTrendingUp && '+'}
75+
{formatPercentage(coin.price_change_percentage_24h)}
76+
</span>
77+
);
78+
},
79+
},
80+
{
81+
header: 'Market Cap',
82+
cellClassName: 'coins-market-cap',
83+
cell: (coin: CoinMarketData) => formatPrice(coin.market_cap),
84+
},
85+
];
86+
3687
return (
3788
<main className='coins-main'>
3889
<div className='flex flex-col w-full space-y-5'>
3990
<h4 className='text-2xl'>All Coins</h4>
4091
<div className='custom-scrollbar coins-container'>
41-
<Table>
42-
<TableHeader className='coins-header'>
43-
<TableRow className='coins-header-row'>
44-
<TableHead className='coins-header-left'>Rank</TableHead>
45-
<TableHead className='text-purple-100'>Token</TableHead>
46-
<TableHead className='text-purple-100'>Price</TableHead>
47-
<TableHead className='coins-header-right'>24h Change</TableHead>
48-
<TableHead className='coins-header-right'>Market Cap</TableHead>
49-
</TableRow>
50-
</TableHeader>
51-
<TableBody>
52-
{coinsData.map((coin: CoinMarketData) => {
53-
const isTrendingUp = coin.price_change_percentage_24h > 0;
54-
55-
return (
56-
<TableRow key={coin.id} className='coins-row relative'>
57-
<TableCell className='coins-rank'>
58-
#{coin.market_cap_rank}
59-
<Link
60-
href={`/coins/${coin.id}`}
61-
className='absolute inset-0 z-10'
62-
aria-label='View coin'
63-
/>
64-
</TableCell>
65-
<TableCell className='coins-token'>
66-
<div className='coins-token-info'>
67-
<Image
68-
src={coin.image}
69-
alt={coin.name}
70-
width={36}
71-
height={36}
72-
/>
73-
<p className='max-w-full truncate'>
74-
{coin.name} ({coin.symbol.toUpperCase()})
75-
</p>
76-
</div>
77-
</TableCell>
78-
<TableCell className='coins-price'>
79-
{formatPrice(coin.current_price)}
80-
</TableCell>
81-
<TableCell className='font-medium'>
82-
<span
83-
className={cn('coins-change', {
84-
'text-green-600': isTrendingUp,
85-
'text-red-500': !isTrendingUp,
86-
})}
87-
>
88-
{isTrendingUp && '+'}
89-
{formatPercentage(coin.price_change_percentage_24h)}
90-
</span>
91-
</TableCell>
92-
<TableCell className='coins-market-cap'>
93-
{formatPrice(coin.market_cap)}
94-
</TableCell>
95-
</TableRow>
96-
);
97-
})}
98-
</TableBody>
99-
</Table>
92+
<DataTable
93+
columns={columns}
94+
data={coinsData}
95+
rowKey={(coin) => coin.id}
96+
headerClassName='coins-header'
97+
headerRowClassName='coins-header-row'
98+
bodyRowClassName='coins-row relative'
99+
/>
100100
</div>
101101

102102
<CoinsPagination

components/DataTable.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
Table,
3+
TableBody,
4+
TableCell,
5+
TableHead,
6+
TableHeader,
7+
TableRow,
8+
} from '@/components/ui/table';
9+
import { cn } from '@/lib/utils';
10+
11+
export const DataTable = <T,>({
12+
columns,
13+
data,
14+
rowKey,
15+
tableClassName,
16+
headerClassName,
17+
headerRowClassName,
18+
headerCellClassName = 'text-purple-100',
19+
bodyRowClassName,
20+
}: DataTableProps<T>) => {
21+
return (
22+
<Table className={tableClassName}>
23+
<TableHeader className={headerClassName}>
24+
<TableRow className={headerRowClassName}>
25+
{columns.map((column, columnIndex) => (
26+
<TableHead
27+
key={columnIndex}
28+
className={cn(
29+
headerCellClassName,
30+
column.headClassName,
31+
columnIndex === 0 && 'pl-5'
32+
)}
33+
>
34+
{column.header}
35+
</TableHead>
36+
))}
37+
</TableRow>
38+
</TableHeader>
39+
<TableBody>
40+
{data.map((row, rowIndex) => (
41+
<TableRow key={rowKey(row, rowIndex)} className={bodyRowClassName}>
42+
{columns.map((column, columnIndex) => (
43+
<TableCell
44+
key={columnIndex}
45+
className={cn(
46+
column.cellClassName,
47+
columnIndex === 0 && 'pl-5'
48+
)}
49+
>
50+
{column.cell(row, rowIndex)}
51+
</TableCell>
52+
))}
53+
</TableRow>
54+
))}
55+
</TableBody>
56+
</Table>
57+
);
58+
};

0 commit comments

Comments
 (0)