Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 6 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { Suspense } from 'react';
import { Suspense } from 'react';
import CoinOverview from '@/components/home/CoinOverview';
import TrendingCoins from '@/components/home/TrendingCoins';
import {
CategoriesFallback,
CoinOverviewFallback,
TrendingCoinsFallback,
} from '@/components/home/fallback';
import Categories from '@/components/home/Categories';

const Page = async () => {
return (
Expand All @@ -19,7 +21,9 @@ const Page = async () => {
</Suspense>
</section>
<section className="w-full mt-7 space-y-4">
<p>Categories</p>
<Suspense fallback={<CategoriesFallback />}>
<Categories />
</Suspense>
</section>
</main>
);
Expand Down
8 changes: 7 additions & 1 deletion components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const DataTable = <T,>({
key={i}
className={cn(
'bg-dark-400 text-purple-100 py-4 first:pl-5 last:pr-5',
headerCellClassName,
column.headClassName,
)}
>
{column.header}
Expand All @@ -49,7 +51,11 @@ const DataTable = <T,>({
{columns.map((column, columnIndex) => (
<TableCell
key={columnIndex}
className={cn('py-4 first:pl-5 last:pr-5', bodyRowClassName)}
className={cn(
'py-4 first:pl-5 last:pr-5',
bodyCellClassName,
column.cellClassName,
)}
>
{column.cell(row, rowIndex)}
</TableCell>
Expand Down
88 changes: 88 additions & 0 deletions components/home/Categories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { fetcher } from '@/lib/coingecko.actions';
import DataTable from '../DataTable';
import { CategoriesFallback } from './fallback';
import Image from 'next/image';
import { cn, formatCurrency, formatPercentage } from '@/lib/utils';
import { TrendingDown, TrendingUp } from 'lucide-react';

const columns: DataTableColumn<Category>[] = [
{
header: 'Name',
cellClassName: 'category-cell',
cell: (category) => category.name,
},
{
header: 'Top Gainers',
cellClassName: 'top-gainers-cell',
cell: (category) => {
return category.top_3_coins.map((coin, index) => (
<Image
key={category.top_3_coins[index]}
src={coin}
alt={category.top_3_coins[index]}
width={24}
height={24}
/>
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix Image key and improve alt text for accessibility.

Two issues with the Image rendering:

  1. Key: Using the URL (category.top_3_coins[index]) as the React key is fragile. If the same coin URL appears multiple times or positions change, React's reconciliation may behave unexpectedly.

  2. Alt text: The alt prop is set to the URL string, which provides no meaningful description for screen readers and violates accessibility best practices.

♿ Proposed fix for key and alt attributes
-      return category.top_3_coins.map((coin, index) => (
+      return category.top_3_coins.map((coin, index) => {
+        const coinName = category.name || 'coin';
+        return (
         <Image
-          key={category.top_3_coins[index]}
+          key={`${category.name}-coin-${index}`}
           src={coin}
-          alt={category.top_3_coins[index]}
+          alt={`Top coin ${index + 1} in ${coinName} category`}
           width={24}
           height={24}
         />
-      ));
+        );
+      });
📝 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
return category.top_3_coins.map((coin, index) => (
<Image
key={category.top_3_coins[index]}
src={coin}
alt={category.top_3_coins[index]}
width={24}
height={24}
/>
));
return category.top_3_coins.map((coin, index) => {
const coinName = category.name || 'coin';
return (
<Image
key={`${category.name}-coin-${index}`}
src={coin}
alt={`Top coin ${index + 1} in ${coinName} category`}
width={24}
height={24}
/>
);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/home/Categories.tsx` around lines 18 - 26, The map rendering in
category.top_3_coins (inside Categories.tsx) uses the image URL for both key and
alt, which is fragile and inaccessible; update the Image usage in the
category.top_3_coins.map callback to use a stable unique key (e.g., combine
category id/name with the map index or a coin id if available — e.g.,
`${category.id}-${index}`) and replace the alt prop with a meaningful
description (e.g., the coin name or a human-friendly string like
`${category.name} coin ${index + 1}` or extract the filename from the URL) so
screen readers get useful text; ensure you update the JSX that returns the Image
component (the Image element inside category.top_3_coins.map) accordingly.

},
},
{
header: '24h Change',
cellClassName: 'change-header-cell ',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove trailing space from className.

The string 'change-header-cell ' contains an unnecessary trailing space.

🧹 Proposed fix
-    cellClassName: 'change-header-cell ',
+    cellClassName: 'change-header-cell',
📝 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
cellClassName: 'change-header-cell ',
cellClassName: 'change-header-cell',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/home/Categories.tsx` at line 31, The cellClassName value contains
an extraneous trailing space ('change-header-cell '); update the assignment for
cellClassName in Categories.tsx to remove the trailing space so it becomes
'change-header-cell', and verify any class concatenation logic uses explicit
separators instead of relying on that trailing space.

cell: (category) => {
const isTrendingUp = category.market_cap_change_24h > 0;

return (
<div
className={cn(
'change-cell flex items-center gap-1',
isTrendingUp ? 'text-green-500' : 'text-red-500',
)}
>
{isTrendingUp ? (
<TrendingUp width={16} height={16} />
) : (
<TrendingDown width={16} height={16} />
)}
<p>{formatPercentage(category.market_cap_change_24h)}</p>
</div>
);
},
},
{
header: 'Market Cap',
cellClassName: 'market-cap-cell',
cell: (category) => formatCurrency(category.market_cap),
},
{
header: '24h Volume',
cellClassName: 'volume-cell',
cell: (category) => formatCurrency(category.volume_24h),
},
];
const TopCategories = async () => {
let topCategories;
try {
topCategories = await fetcher<Category[]>(
'/coins/categories',
undefined,
300,
);
} catch (error) {
console.error('Error fetching top categories:', error);
return <CategoriesFallback />;
}
return (
<div id="categories" className="custom-scrollbar">
<h4>Top Categories</h4>
<DataTable
data={topCategories.slice(0, 10)}
columns={columns}
rowKey={(category) => category.name}
tableClassName="mt-3"
/>
</div>
);
};

export default TopCategories;
6 changes: 3 additions & 3 deletions components/home/TrendingCoins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fetcher } from '@/lib/coingecko.actions';
import Image from 'next/image';
import Link from 'next/link';
import { TrendingDown, TrendingUp } from 'lucide-react';
import { cn, formatCurrency } from '@/lib/utils';
import { cn, formatCurrency, formatPercentage } from '@/lib/utils';
import { TrendingCoinsFallback } from '@/components/home/fallback';

const columns: DataTableColumn<TrendingCoin>[] = [
Expand Down Expand Up @@ -40,7 +40,7 @@ const columns: DataTableColumn<TrendingCoin>[] = [
) : (
<TrendingDown width={16} height={16} />
)}
<p>{item.data.price_change_percentage_24h.usd.toFixed(2)}%</p>
<p>{formatPercentage(item.data.price_change_percentage_24h.usd)}</p>
</div>
);
},
Expand Down Expand Up @@ -69,7 +69,7 @@ const TrendingCoins = async () => {
<h4>Trending Coins</h4>

<DataTable
data={trendingCoins.coins.slice(0, 6) || []}
data={trendingCoins.coins.slice(0, 6)}
columns={columns}
rowKey={(coin) => coin.item.id}
tableClassName="trending-coins-table"
Expand Down