Skip to content
Open
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
18 changes: 18 additions & 0 deletions .vscode/settings.json
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"
}
}
129 changes: 116 additions & 13 deletions app/coins/[id]/page.tsx
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',
},
];
Comment on lines +26 to +57
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add null safety for API data access.

The coinDetails array accesses deeply nested properties and array elements from coinData without 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 coinDetails = [
{
label: 'Market Cap',
value: formatPrice(coinData.market_data?.market_cap?.usd),
},
{
label: 'Market Cap Rank',
value: coinData.market_cap_rank ? `# ${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',
},
];
🤖 Prompt for AI Agents
In app/coins/[id]/page.tsx around lines 25 to 56, the coinDetails array reads
deeply nested properties and array items from coinData without null checks,
which can crash if the API returns missing fields; update each access to use
optional chaining and nullish coalescing (e.g., market_data?.market_cap?.usd ??
defaultValue, market_cap_rank ?? '-', market_data?.total_volume?.usd ??
defaultValue) and guard array accesses with optional chaining for indexes (e.g.,
links?.homepage?.[0] ?? undefined or '-') so values and links default safely
when absent; ensure formatPrice is only called with a defined numeric value or
wrap it so it handles undefined safely.


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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Empty Link element has no clickable content.

The <Link> component at lines 67-71 has no children, making it invisible and un-clickable. The user won't be able to navigate to the exchange.

Additionally, ticker.trade_url should be validated before use.

🔎 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
In app/coins/[id]/page.tsx around lines 63 to 73, the Link component is empty
and therefore not clickable and the trade URL is used without validation; update
the cell renderer so the Link contains visible, accessible content (e.g., link
text or wrap the market name) and ensure ticker.trade_url is validated
(non-empty and a safe URL) before rendering the Link; if the URL is missing or
invalid, render a disabled/hidden link or fallback UI instead to avoid rendering
a broken anchor.

},
{
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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against empty or invalid link values.

The conditional {link ? ...} will pass for empty strings (""), resulting in invalid href="" on the Link. Consider a stricter check.

🔎 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 rel='noopener noreferrer' for external links opened in new tabs for security best practices.

🤖 Prompt for AI Agents
In app/coins/[id]/page.tsx around lines 137 to 143, the current check {link ?
...} allows empty strings and invalid values which can produce href="" and a bad
external link; replace this with a stricter truthy and URL-safe check (e.g.,
ensure link is a non-empty string and optionally starts with http:// or https://
or use a URL validator) before rendering the Link, and when rendering external
links with target='_blank' add rel='noopener noreferrer' to the Link component
for security.

) : (
<p className='text-base font-medium'>{value}</p>
)}
</li>
))}
</ul>
</div>

{/* Top Gainers / Losers */}
<TopGainersLosers />
</section>
</main>
Expand Down
140 changes: 68 additions & 72 deletions app/coins/page.tsx
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,
Expand All @@ -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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Empty <Link> component will throw an error and be non-functional.

Next.js requires children when using the Link component. The self-closing <Link> on line 36 has no children, which causes Next.js to throw the error "No children were passed to <Link> with href of [...] but one child is required" during development.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cell: (coin: CoinMarketData) => (
<>
#{coin.market_cap_rank}
<Link href={`/coins/${coin.id}`} aria-label='View coin' />
</>
),
cell: (coin: CoinMarketData) => (
<div className="relative">
#{coin.market_cap_rank}
<Link
href={`/coins/${coin.id}`}
aria-label='View coin'
className="absolute inset-0"
>
<span className="sr-only">View coin</span>
</Link>
</div>
),
🤖 Prompt for AI Agents
In app/coins/page.tsx around lines 33 to 38, the Link is self-closing which
causes Next.js to throw "No children were passed to <Link>" — replace the
self-closing Link with a proper element that contains children: either wrap the
existing cell content inside the Link (so the rank and any clickable area are
children of Link) or keep the visible content and add a child element (e.g., a
span or visually-hidden text) inside the Link for accessibility; if you intended
a full-row overlay, render the Link as a positioned container (absolute inset)
with a child element that covers the cell and includes accessible text. Ensure
the Link is not empty and includes accessible children (visible or
visually-hidden) so Next.js no longer errors.

},
{
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}
Expand Down
Loading