-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor/codebase v2 #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eefebc5
6c0893a
58ac76f
f25651d
cbe15e5
37ca69c
0fbe8a4
6b1632d
227116e
fc28bc4
a2da611
84ad300
d2de97f
3525d0b
58315f8
cea1a09
4b16d3f
ea7aa23
dc5fa21
b77f4ee
8c06fe9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "editor.defaultFormatter": "esbenp.prettier-vscode", | ||
| "editor.formatOnSave": true, | ||
| "editor.codeActionsOnSave": { | ||
| "source.fixAll.eslint": "explicit", | ||
| "source.addMissingImports": "explicit" | ||
| }, | ||
| "prettier.tabWidth": 2, | ||
| "prettier.useTabs": false, | ||
| "prettier.semi": true, | ||
| "prettier.singleQuote": true, | ||
| "prettier.jsxSingleQuote": true, | ||
| "prettier.trailingComma": "es5", | ||
| "prettier.arrowParens": "always", | ||
| "[typescriptreact]": { | ||
| "editor.defaultFormatter": "esbenp.prettier-vscode" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,49 +1,152 @@ | ||
| import Link from 'next/link'; | ||
| import { ArrowUpRight } from 'lucide-react'; | ||
|
|
||
| import { | ||
| getCoinDetails, | ||
| getCoinOHLC, | ||
| fetchPools, | ||
| fetchTopPool, | ||
| } from '@/lib/coingecko.actions'; | ||
| import { Converter } from '@/components/Converter'; | ||
| import { Converter } from '@/components/coin-details/Converter'; | ||
| import LiveDataWrapper from '@/components/LiveDataWrapper'; | ||
| import { ExchangeListings } from '@/components/ExchangeListings'; | ||
| import { CoinDetailsSection } from '@/components/CoinDetailsSection'; | ||
| import { TopGainersLosers } from '@/components/TopGainersLosers'; | ||
| import { TopGainersLosers } from '@/components/coin-details/TopGainersLosers'; | ||
| import { DataTable } from '@/components/DataTable'; | ||
| import { formatPrice, timeAgo } from '@/lib/utils'; | ||
|
|
||
| const CoinDetails = async ({ params }: { params: Promise<{ id: string }> }) => { | ||
| const { id } = await params; | ||
| const coinData = await getCoinDetails(id); | ||
|
|
||
| const pool = coinData.asset_platform_id | ||
| ? await fetchTopPool(coinData.asset_platform_id, coinData.contract_address) | ||
| : await fetchPools(id); | ||
|
|
||
| const coinOHLCData = await getCoinOHLC(id, 1, 'usd', 'hourly', 'full'); | ||
|
|
||
| const coinDetails = [ | ||
| { | ||
| label: 'Market Cap', | ||
| value: formatPrice(coinData.market_data.market_cap.usd), | ||
| }, | ||
| { | ||
| label: 'Market Cap Rank', | ||
| value: `# ${coinData.market_cap_rank}`, | ||
| }, | ||
| { | ||
| label: 'Total Volume', | ||
| value: formatPrice(coinData.market_data.total_volume.usd), | ||
| }, | ||
| { | ||
| label: 'Website', | ||
| value: '-', | ||
| link: coinData.links.homepage[0], | ||
| linkText: 'Website', | ||
| }, | ||
| { | ||
| label: 'Explorer', | ||
| value: '-', | ||
| link: coinData.links.blockchain_site[0], | ||
| linkText: 'Explorer', | ||
| }, | ||
| { | ||
| label: 'Community Link', | ||
| value: '-', | ||
| link: coinData.links.subreddit_url, | ||
| linkText: 'Community', | ||
| }, | ||
| ]; | ||
|
|
||
| const exchangeColumns = [ | ||
| { | ||
| header: 'Exchange', | ||
| cellClassName: 'exchange-name', | ||
| cell: (ticker: Ticker) => ( | ||
| <> | ||
| {ticker.market.name} | ||
|
|
||
| <Link | ||
| href={ticker.trade_url} | ||
| target='_blank' | ||
| aria-label='View coin' | ||
| /> | ||
| </> | ||
| ), | ||
|
Comment on lines
+63
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty Link element has no clickable content. The Additionally, 🔎 Proposed fix to make the link functional cell: (ticker: Ticker) => (
- <>
- {ticker.market.name}
-
- <Link
- href={ticker.trade_url}
- target='_blank'
- aria-label='View coin'
- />
- </>
+ <Link
+ href={ticker.trade_url ?? '#'}
+ target='_blank'
+ aria-label={`View ${ticker.market?.name} exchange`}
+ className='exchange-link'
+ >
+ {ticker.market?.name ?? '-'}
+ <ArrowUpRight size={14} />
+ </Link>
),🤖 Prompt for AI Agents |
||
| }, | ||
| { | ||
| header: 'Pair', | ||
| cell: (ticker: Ticker) => ( | ||
| <div className='pair'> | ||
| <p>{ticker.base}</p> | ||
| <p>{ticker.target}</p> | ||
| </div> | ||
| ), | ||
| }, | ||
| { | ||
| header: 'Price', | ||
| cellClassName: 'price-cell', | ||
| cell: (ticker: Ticker) => formatPrice(ticker.converted_last.usd), | ||
| }, | ||
| { | ||
| header: 'Last Traded', | ||
| headClassName: 'text-end', | ||
| cellClassName: 'time-cell', | ||
| cell: (ticker: Ticker) => timeAgo(ticker.timestamp), | ||
| }, | ||
| ]; | ||
|
|
||
| return ( | ||
| <main className='coin-details-main'> | ||
| <section className='size-full xl:col-span-2'> | ||
| <main id='coin-details-page'> | ||
| <section className='primary'> | ||
| <LiveDataWrapper | ||
| coinId={id} | ||
| poolId={pool.id} | ||
| coin={coinData} | ||
| coinOHLCData={coinOHLCData} | ||
| > | ||
| {/* Exchange Listings - pass it as a child of a client component so it will be render server side */} | ||
| <ExchangeListings coinData={coinData} /> | ||
| <div className='exchange-section'> | ||
| <h4>Exchange Listings</h4> | ||
|
|
||
| <DataTable | ||
| tableClassName='exchange-table' | ||
| columns={exchangeColumns} | ||
| data={coinData.tickers.slice(0, 7)} | ||
| rowKey={(_, index) => index} | ||
| bodyCellClassName='py-2!' | ||
| /> | ||
| </div> | ||
| </LiveDataWrapper> | ||
| </section> | ||
|
|
||
| <section className='size-full max-lg:mt-8 lg:col-span-1'> | ||
| {/* Converter */} | ||
| <section className='secondary'> | ||
| <Converter | ||
| symbol={coinData.symbol} | ||
| icon={coinData.image.small} | ||
| priceList={coinData.market_data.current_price} | ||
| /> | ||
|
|
||
| {/* Coin Details */} | ||
| <CoinDetailsSection coinData={coinData} /> | ||
| <div className='details'> | ||
| <h4>Coin Details</h4> | ||
|
|
||
| <ul className='details-grid'> | ||
| {coinDetails.map(({ label, value, link, linkText }, index) => ( | ||
| <li key={index}> | ||
| <p className='label'>{label}</p> | ||
|
|
||
| {link ? ( | ||
| <div className='link'> | ||
| <Link href={link} target='_blank'> | ||
| {linkText || label} | ||
| </Link> | ||
| <ArrowUpRight size={16} /> | ||
| </div> | ||
|
Comment on lines
+135
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against empty or invalid link values. The conditional 🔎 Proposed fix with stricter validation- {link ? (
+ {link && link.trim() !== '' ? (
<div className='link'>
- <Link href={link} target='_blank'>
+ <Link href={link} target='_blank' rel='noopener noreferrer'>
{linkText || label}
</Link>
<ArrowUpRight size={16} />
</div>Also, consider adding 🤖 Prompt for AI Agents |
||
| ) : ( | ||
| <p className='text-base font-medium'>{value}</p> | ||
| )} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
|
|
||
| {/* Top Gainers / Losers */} | ||
| <TopGainersLosers /> | ||
| </section> | ||
| </main> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,10 @@ | ||||||||||||||||||||||||||||||||||||||
| import { getCoinList } from '@/lib/coingecko.actions'; | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| Table, | ||||||||||||||||||||||||||||||||||||||
| TableBody, | ||||||||||||||||||||||||||||||||||||||
| TableCell, | ||||||||||||||||||||||||||||||||||||||
| TableHead, | ||||||||||||||||||||||||||||||||||||||
| TableHeader, | ||||||||||||||||||||||||||||||||||||||
| TableRow, | ||||||||||||||||||||||||||||||||||||||
| } from '@/components/ui/table'; | ||||||||||||||||||||||||||||||||||||||
| import { DataTable } from '@/components/DataTable'; | ||||||||||||||||||||||||||||||||||||||
| import Image from 'next/image'; | ||||||||||||||||||||||||||||||||||||||
| import { cn, formatPercentage, formatPrice } from '@/lib/utils'; | ||||||||||||||||||||||||||||||||||||||
| import Link from 'next/link'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import CoinsPagination from '@/components/CoinsPagination'; | ||||||||||||||||||||||||||||||||||||||
| import { ClickableTableRow } from '@/components/ClickableTableRow'; | ||||||||||||||||||||||||||||||||||||||
| import { cn, formatPercentage, formatPrice } from '@/lib/utils'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const Coins = async ({ | ||||||||||||||||||||||||||||||||||||||
| searchParams, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -32,69 +26,71 @@ const Coins = async ({ | |||||||||||||||||||||||||||||||||||||
| const estimatedTotalPages = | ||||||||||||||||||||||||||||||||||||||
| currentPage >= 100 ? Math.ceil(currentPage / 100) * 100 + 100 : 100; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <main className='coins-main'> | ||||||||||||||||||||||||||||||||||||||
| <div className='flex flex-col w-full space-y-5'> | ||||||||||||||||||||||||||||||||||||||
| <h4 className='text-2xl'>All Coins</h4> | ||||||||||||||||||||||||||||||||||||||
| <div className='custom-scrollbar coins-container'> | ||||||||||||||||||||||||||||||||||||||
| <Table> | ||||||||||||||||||||||||||||||||||||||
| <TableHeader className='coins-header'> | ||||||||||||||||||||||||||||||||||||||
| <TableRow className='coins-header-row'> | ||||||||||||||||||||||||||||||||||||||
| <TableHead className='coins-header-left'>Rank</TableHead> | ||||||||||||||||||||||||||||||||||||||
| <TableHead className='text-purple-100'>Token</TableHead> | ||||||||||||||||||||||||||||||||||||||
| <TableHead className='text-purple-100'>Price</TableHead> | ||||||||||||||||||||||||||||||||||||||
| <TableHead className='coins-header-right'>24h Change</TableHead> | ||||||||||||||||||||||||||||||||||||||
| <TableHead className='coins-header-right'>Market Cap</TableHead> | ||||||||||||||||||||||||||||||||||||||
| </TableRow> | ||||||||||||||||||||||||||||||||||||||
| </TableHeader> | ||||||||||||||||||||||||||||||||||||||
| <TableBody> | ||||||||||||||||||||||||||||||||||||||
| {coinsData.map((coin: CoinMarketData) => { | ||||||||||||||||||||||||||||||||||||||
| const isTrendingUp = coin.price_change_percentage_24h > 0; | ||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <ClickableTableRow | ||||||||||||||||||||||||||||||||||||||
| key={coin.id} | ||||||||||||||||||||||||||||||||||||||
| href={`/coins/${coin.id}`} | ||||||||||||||||||||||||||||||||||||||
| className='coins-row' | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <TableCell className='coins-rank'> | ||||||||||||||||||||||||||||||||||||||
| #{coin.market_cap_rank} | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| <TableCell className='coins-token'> | ||||||||||||||||||||||||||||||||||||||
| <div className='coins-token-info'> | ||||||||||||||||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||||||||||||||||
| src={coin.image} | ||||||||||||||||||||||||||||||||||||||
| alt={coin.name} | ||||||||||||||||||||||||||||||||||||||
| width={36} | ||||||||||||||||||||||||||||||||||||||
| height={36} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| <p className='max-w-[100%] truncate'> | ||||||||||||||||||||||||||||||||||||||
| {coin.name} ({coin.symbol.toUpperCase()}) | ||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| <TableCell className='coins-price'> | ||||||||||||||||||||||||||||||||||||||
| {formatPrice(coin.current_price)} | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| <TableCell className='font-medium'> | ||||||||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||||||||
| className={cn('coins-change', { | ||||||||||||||||||||||||||||||||||||||
| 'text-green-600': isTrendingUp, | ||||||||||||||||||||||||||||||||||||||
| 'text-red-500': !isTrendingUp, | ||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {isTrendingUp && '+'} | ||||||||||||||||||||||||||||||||||||||
| {formatPercentage(coin.price_change_percentage_24h)} | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| <TableCell className='coins-market-cap'> | ||||||||||||||||||||||||||||||||||||||
| {formatPrice(coin.market_cap)} | ||||||||||||||||||||||||||||||||||||||
| </TableCell> | ||||||||||||||||||||||||||||||||||||||
| </ClickableTableRow> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||
| </TableBody> | ||||||||||||||||||||||||||||||||||||||
| </Table> | ||||||||||||||||||||||||||||||||||||||
| const columns = [ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| header: 'Rank', | ||||||||||||||||||||||||||||||||||||||
| cellClassName: 'rank-cell', | ||||||||||||||||||||||||||||||||||||||
| cell: (coin: CoinMarketData) => ( | ||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||
| #{coin.market_cap_rank} | ||||||||||||||||||||||||||||||||||||||
| <Link href={`/coins/${coin.id}`} aria-label='View coin' /> | ||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty Next.js requires children when using the Link component. The self-closing If you intend this as a row overlay, wrap it in a container with absolute positioning: cell: (coin: CoinMarketData) => (
- <>
+ <div className="relative">
#{coin.market_cap_rank}
- <Link href={`/coins/${coin.id}`} aria-label='View coin' />
- </>
+ <Link
+ href={`/coins/${coin.id}`}
+ aria-label='View coin'
+ className="absolute inset-0"
+ >
+ <span className="sr-only">View coin</span>
+ </Link>
+ </div>
),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| header: 'Token', | ||||||||||||||||||||||||||||||||||||||
| cellClassName: 'token-cell', | ||||||||||||||||||||||||||||||||||||||
| cell: (coin: CoinMarketData) => ( | ||||||||||||||||||||||||||||||||||||||
| <div className='token-info'> | ||||||||||||||||||||||||||||||||||||||
| <Image src={coin.image} alt={coin.name} width={36} height={36} /> | ||||||||||||||||||||||||||||||||||||||
| <p> | ||||||||||||||||||||||||||||||||||||||
| {coin.name} ({coin.symbol.toUpperCase()}) | ||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| header: 'Price', | ||||||||||||||||||||||||||||||||||||||
| cellClassName: 'price-cell', | ||||||||||||||||||||||||||||||||||||||
| cell: (coin: CoinMarketData) => formatPrice(coin.current_price), | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| header: '24h Change', | ||||||||||||||||||||||||||||||||||||||
| cellClassName: 'change-cell', | ||||||||||||||||||||||||||||||||||||||
| cell: (coin: CoinMarketData) => { | ||||||||||||||||||||||||||||||||||||||
| const isTrendingUp = coin.price_change_percentage_24h > 0; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||||||||
| className={cn('change-value', { | ||||||||||||||||||||||||||||||||||||||
| 'text-green-600': isTrendingUp, | ||||||||||||||||||||||||||||||||||||||
| 'text-red-500': !isTrendingUp, | ||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {isTrendingUp && '+'} | ||||||||||||||||||||||||||||||||||||||
| {formatPercentage(coin.price_change_percentage_24h)} | ||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| header: 'Market Cap', | ||||||||||||||||||||||||||||||||||||||
| cellClassName: 'market-cap-cell', | ||||||||||||||||||||||||||||||||||||||
| cell: (coin: CoinMarketData) => formatPrice(coin.market_cap), | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <main id='coins-page'> | ||||||||||||||||||||||||||||||||||||||
| <div className='content'> | ||||||||||||||||||||||||||||||||||||||
| <h4>All Coins</h4> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <DataTable | ||||||||||||||||||||||||||||||||||||||
| tableClassName='coins-table' | ||||||||||||||||||||||||||||||||||||||
| columns={columns} | ||||||||||||||||||||||||||||||||||||||
| data={coinsData} | ||||||||||||||||||||||||||||||||||||||
| rowKey={(coin) => coin.id} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <CoinsPagination | ||||||||||||||||||||||||||||||||||||||
| currentPage={currentPage} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add null safety for API data access.
The
coinDetailsarray accesses deeply nested properties and array elements fromcoinDatawithout null/undefined checks. If the API response is incomplete or has a different structure, this will cause runtime errors.🔎 Proposed fix using optional chaining and null coalescing
const coinDetails = [ { label: 'Market Cap', - value: formatPrice(coinData.market_data.market_cap.usd), + value: formatPrice(coinData.market_data?.market_cap?.usd), }, { label: 'Market Cap Rank', - value: `# ${coinData.market_cap_rank}`, + value: coinData.market_cap_rank ? `# ${coinData.market_cap_rank}` : '-', }, { label: 'Total Volume', - value: formatPrice(coinData.market_data.total_volume.usd), + value: formatPrice(coinData.market_data?.total_volume?.usd), }, { label: 'Website', value: '-', - link: coinData.links.homepage[0], + link: coinData.links?.homepage?.[0], linkText: 'Website', }, { label: 'Explorer', value: '-', - link: coinData.links.blockchain_site[0], + link: coinData.links?.blockchain_site?.[0], linkText: 'Explorer', }, { label: 'Community Link', value: '-', - link: coinData.links.subreddit_url, + link: coinData.links?.subreddit_url, linkText: 'Community', }, ];📝 Committable suggestion
🤖 Prompt for AI Agents