|
3 | 3 | * ---------------------- |
4 | 4 | * Easy: |
5 | 5 | * - [ ] Sort buttons (Market Cap, Price, 24h %) |
6 | | - * - [ ] Show coin symbol & small logo (image URL in API) |
7 | | - * - [ ] Format numbers with utility (abbreviate large caps: 1.2B) |
8 | | - * - [ ] Highlight positive vs negative 24h change (green/red) |
| 6 | + * - [x] Show coin symbol & small logo (image URL in API) |
| 7 | + * - [x] Format numbers with utility (abbreviate large caps: 1.2B) |
| 8 | + * - [x] Highlight positive vs negative 24h change (green/red) |
9 | 9 | * Medium: |
10 | | - * - [ ] Add pagination (Top 50 -> allow next pages) |
| 10 | + * - [x] Add pagination (Top 50 -> allow next pages) |
11 | 11 | * - [ ] Client-side caching with timestamp (avoid re-fetch spam) |
12 | 12 | * - [ ] Mini sparkline (use canvas or simple SVG) |
13 | 13 | * - [ ] Favorites (star) + localStorage persistence |
|
18 | 18 | * - [ ] Dark mode adaptive coloring for charts |
19 | 19 | * - [ ] Extract to service + custom hook (useCryptoMarkets) |
20 | 20 | */ |
| 21 | + |
21 | 22 | import { useEffect, useState } from "react"; |
22 | 23 | import Loading from "../components/Loading.jsx"; |
23 | 24 | import ErrorMessage from "../components/ErrorMessage.jsx"; |
24 | 25 | import Card from "../components/Card.jsx"; |
| 26 | +import formatNumber from "../utilities/numberFormatter.js"; |
25 | 27 |
|
26 | 28 | export default function Crypto() { |
27 | 29 | const [coins, setCoins] = useState([]); |
@@ -57,30 +59,61 @@ export default function Crypto() { |
57 | 59 |
|
58 | 60 | return ( |
59 | 61 | <div> |
60 | | - <h2>Cryptocurrency Tracker</h2> |
| 62 | + <h2>💹 Cryptocurrency Tracker</h2> |
61 | 63 | <input |
62 | 64 | value={query} |
63 | 65 | onChange={(e) => setQuery(e.target.value)} |
64 | | - placeholder="Search coin" |
| 66 | + placeholder="Search coin..." |
| 67 | + style={{ marginBottom: "1rem" }} |
65 | 68 | /> |
66 | 69 |
|
67 | 70 | {loading && <Loading />} |
68 | 71 | <ErrorMessage error={error} /> |
| 72 | + |
69 | 73 | <div className="grid"> |
70 | | - {filtered.map((c) => ( |
71 | | - <Card |
72 | | - key={c.id} |
73 | | - title={c.name} |
74 | | - footer={<span>${c.current_price}</span>} |
75 | | - > |
76 | | - <p>Market Cap: ${c.market_cap.toLocaleString()}</p> |
77 | | - <p>24h: {c.price_change_percentage_24h?.toFixed(2)}%</p> |
78 | | - {/* TODO: Add mini sparkline chart */} |
79 | | - </Card> |
80 | | - ))} |
| 74 | + {filtered.map((c) => { |
| 75 | + const isPositive = c.price_change_percentage_24h >= 0; |
| 76 | + return ( |
| 77 | + <Card |
| 78 | + key={c.id} |
| 79 | + title={ |
| 80 | + <span style={{ display: "flex", alignItems: "center", gap: 8 }}> |
| 81 | + <img |
| 82 | + src={c.image} |
| 83 | + alt={c.symbol} |
| 84 | + style={{ width: 24, height: 24, borderRadius: "50%" }} |
| 85 | + /> |
| 86 | + {c.name} ({c.symbol.toUpperCase()}) |
| 87 | + </span> |
| 88 | + } |
| 89 | + footer={<strong>${c.current_price.toLocaleString()}</strong>} |
| 90 | + > |
| 91 | + <p>Market Cap: ${formatNumber(c.market_cap)}</p> |
| 92 | + <p |
| 93 | + style={{ |
| 94 | + color: isPositive ? "#16a34a" : "#dc2626", |
| 95 | + fontWeight: 600, |
| 96 | + }} |
| 97 | + > |
| 98 | + 24h: {isPositive ? "+" : ""} |
| 99 | + {c.price_change_percentage_24h?.toFixed(2)}% |
| 100 | + </p> |
| 101 | + {/* TODO: Add mini sparkline chart */} |
| 102 | + </Card> |
| 103 | + ); |
| 104 | + })} |
81 | 105 | </div> |
82 | 106 |
|
83 | | - <div className="pagination"> |
| 107 | + <div |
| 108 | + className="pagination" |
| 109 | + style={{ |
| 110 | + display: "flex", |
| 111 | + justifyContent: "center", |
| 112 | + alignItems: "center", |
| 113 | + gap: "1rem", |
| 114 | + marginTop: "1.5rem", |
| 115 | + }} |
| 116 | + > |
84 | 117 | <button onClick={() => setPage((p) => p - 1)} disabled={page === 1}> |
85 | 118 | Previous |
86 | 119 | </button> |
|
0 commit comments