diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..581af7c --- /dev/null +++ b/.vscode/settings.json @@ -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" + } +} diff --git a/app/coins/[id]/page.tsx b/app/coins/[id]/page.tsx index 6797525..1e69cc7 100644 --- a/app/coins/[id]/page.tsx +++ b/app/coins/[id]/page.tsx @@ -1,204 +1,163 @@ -import { - getCoinDetails, - getCoinOHLC, - fetchPools, - getTopGainersLosers, -} from '@/lib/coingecko.actions'; -import { formatPrice, timeAgo } from '@/lib/utils'; import Link from 'next/link'; -import CoinDetailCard from '@/components/CoinDetailCard'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { Converter } from '@/components/Converter'; +import { ArrowUpRight } from 'lucide-react'; + +import { fetcher, getPools } from '@/lib/coingecko.actions'; +import { Converter } from '@/components/coin-details/Converter'; import LiveDataWrapper from '@/components/LiveDataWrapper'; -import CoinCard from '@/components/CoinCard'; +import { TopGainersLosers } from '@/components/coin-details/TopGainersLosers'; +import { DataTable } from '@/components/DataTable'; +import { formatCurrency, timeAgo } from '@/lib/utils'; -const CoinDetails = async ({ params }: { params: Promise<{ id: string }> }) => { +const CoinDetails = async ({ params }: NextPageProps) => { const { id } = await params; - const coinData = await getCoinDetails(id); - const topGainersLosers = await getTopGainersLosers(); - const coinOHLCData = await getCoinOHLC(id, 30, 'usd', 'hourly', 'full'); - const pool = await fetchPools(id); - - const coin = { - id: coinData.id, - name: coinData.name, - symbol: coinData.symbol, - image: coinData.image.large, - icon: coinData.image.small, - price: coinData.market_data.current_price.usd, - priceList: coinData.market_data.current_price, - priceChange24h: coinData.market_data.price_change_24h_in_currency.usd, - priceChangePercentage24h: - coinData.market_data.price_change_percentage_24h_in_currency.usd, - priceChangePercentage30d: - coinData.market_data.price_change_percentage_30d_in_currency.usd, - marketCap: coinData.market_data.market_cap.usd, - marketCapRank: coinData.market_cap_rank, - description: coinData.description.en, - totalVolume: coinData.market_data.total_volume.usd, - website: coinData.links.homepage[0], - explorer: coinData.links.blockchain_site[0], - communityLink: coinData.links.subreddit_url, - tickers: coinData.tickers, - }; + + const [coinData, coinOHLCData] = await Promise.all([ + fetcher(`/coins/${id}`, { + dex_pair_format: 'contract_address', + }), + fetcher(`/coins/${id}/ohlc`, { + vs_currency: 'usd', + days: 1, + interval: 'hourly', + precision: 'full', + }), + ]); + + const platform = coinData.asset_platform_id + ? coinData.detail_platforms?.[coinData.asset_platform_id] + : null; + + const network = platform?.geckoterminal_url.split('/')[3] || null; + const contractAddress = platform?.contract_address || null; + + const pool = await getPools(id, network, contractAddress); + + const coinDetails = [ + { + label: 'Market Cap', + value: formatCurrency(coinData.market_data.market_cap.usd), + }, + { + label: 'Market Cap Rank', + value: `# ${coinData.market_cap_rank}`, + }, + { + label: 'Total Volume', + value: formatCurrency(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: DataTableColumn[] = [ + { + header: 'Exchange', + cellClassName: 'exchange-name', + cell: (ticker) => ( + <> + {ticker.market.name} + + + + ), + }, + { + header: 'Pair', + cell: (ticker) => ( +
+

{ticker.base}

+ / +

{ticker.target}

+
+ ), + }, + { + header: 'Price', + cellClassName: 'price-cell', + cell: (ticker) => formatCurrency(ticker.converted_last.usd), + }, + { + header: 'Last Traded', + headClassName: 'text-end', + cellClassName: 'time-cell', + cell: (ticker) => timeAgo(ticker.timestamp), + }, + ]; return ( -
-
+
+
- {/* Exchange Listings */} -
-

Exchange Listings

-
- - - - - Exchange - - Pair - Price - - Last Traded - - - - - {coin.tickers - .slice(0, 7) - .map((ticker: Ticker, index: number) => ( - - - - {ticker.market.name} - - - -
-

- {ticker.base} -

- / -

- {ticker.target} -

-
-
- - {formatPrice(ticker.converted_last.usd)} - - - {timeAgo(ticker.timestamp)} - -
- ))} -
-
-
+
+

Exchange Listings

+ + index} + bodyCellClassName='py-2!' + />
-
- {/* Converter */} -
-

- {coin.symbol.toUpperCase()} Converter -

- -
+
+ - {/* Coin Details */} -
-

Coin Details

-
- {[ - { - label: 'Market Cap', - value: formatPrice(coin.marketCap), - }, - { - label: 'Market Cap Rank', - value: `# ${coin.marketCapRank}`, - }, - { - label: 'Total Volume', - value: formatPrice(coin.totalVolume), - }, - { - label: 'Website', - value: '-', - link: coin.website, - linkText: 'Website', - }, - { - label: 'Explorer', - value: '-', - link: coin.explorer, - linkText: 'Explorer', - }, - { - label: 'Community Link', - value: '-', - link: coin.communityLink, - linkText: 'Community', - }, - ].map((detail, index) => ( - +
+

Coin Details

+ +
    + {coinDetails.map(({ label, value, link, linkText }, index) => ( +
  • +

    {label}

    + + {link ? ( +
    + + {linkText || label} + + +
    + ) : ( +

    {value}

    + )} +
  • ))} -
+
- {/* Top Gainers */} -
-

Top Gainers

-
- {topGainersLosers.top_gainers.map( - (coin: TopGainersLosersResponse) => ( - - ) - )} -
-
+
); diff --git a/app/coins/page.tsx b/app/coins/page.tsx index 6bb1f9f..f120248 100644 --- a/app/coins/page.tsx +++ b/app/coins/page.tsx @@ -1,27 +1,25 @@ -import { getCoinList } from '@/lib/coingecko.actions'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { fetcher } from '@/lib/coingecko.actions'; +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, formatCurrency } from '@/lib/utils'; + +const Coins = async ({ searchParams }: NextPageProps) => { + const { page } = await searchParams; -const Coins = async ({ - searchParams, -}: { - searchParams: Promise<{ page?: string }>; -}) => { - const params = await searchParams; - const currentPage = Number(params.page) || 1; + const currentPage = Number(page) || 1; const perPage = 10; - const coinsData = await getCoinList(currentPage, perPage); + const coinsData = await fetcher('/coins/markets', { + vs_currency: 'usd', + order: 'market_cap_desc', + per_page: perPage, + page: currentPage, + sparkline: 'false', + price_change_percentage: '24h', + }); // CoinGecko API doesn't return total count, so we determine pagination dynamically: // - If we receive fewer items than perPage, we're on the last page @@ -32,69 +30,71 @@ const Coins = async ({ const estimatedTotalPages = currentPage >= 100 ? Math.ceil(currentPage / 100) * 100 + 100 : 100; - return ( -
-
-

All Coins

-
- - - - Rank - Token - Price - 24h Change - Market Cap - - - - {coinsData.map((coin: CoinMarketData) => { - const isTrendingUp = coin.price_change_percentage_24h > 0; - return ( - - - #{coin.market_cap_rank} - - -
- -

- {coin.name} ({coin.symbol.toUpperCase()}) -

-
-
- - {formatPrice(coin.current_price)} - - - - {isTrendingUp && '+'} - {formatPercentage(coin.price_change_percentage_24h)} - - - - {formatPrice(coin.market_cap)} - -
- ); - })} -
-
+ const columns: DataTableColumn[] = [ + { + header: 'Rank', + cellClassName: 'rank-cell', + cell: (coin) => ( + <> + #{coin.market_cap_rank} + + + ), + }, + { + header: 'Token', + cellClassName: 'token-cell', + cell: (coin) => ( +
+ {coin.name} +

+ {coin.name} ({coin.symbol.toUpperCase()}) +

+ ), + }, + { + header: 'Price', + cellClassName: 'price-cell', + cell: (coin) => formatCurrency(coin.current_price), + }, + { + header: '24h Change', + cellClassName: 'change-cell', + cell: (coin) => { + const isTrendingUp = coin.price_change_percentage_24h > 0; + + return ( + + {isTrendingUp && '+'} + {formatPercentage(coin.price_change_percentage_24h)} + + ); + }, + }, + { + header: 'Market Cap', + cellClassName: 'market-cap-cell', + cell: (coin) => formatCurrency(coin.market_cap), + }, + ]; + + return ( +
+
+

All Coins

+ + coin.id} + />