Skip to content

Commit 5e5b9ba

Browse files
Merge branch 'main' into feature/compact-number-format
2 parents cfe909c + b264b3c commit 5e5b9ba

File tree

2 files changed

+121
-25
lines changed

2 files changed

+121
-25
lines changed

src/pages/Crypto.jsx

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* ----------------------
44
* Easy:
55
* - [ ] 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)
99
* Medium:
10-
* - [ ] Add pagination (Top 50 -> allow next pages)
10+
* - [x] Add pagination (Top 50 -> allow next pages)
1111
* - [ ] Client-side caching with timestamp (avoid re-fetch spam)
1212
* - [ ] Mini sparkline (use canvas or simple SVG)
1313
* - [ ] Favorites (star) + localStorage persistence
@@ -18,45 +18,109 @@
1818
* - [ ] Dark mode adaptive coloring for charts
1919
* - [ ] Extract to service + custom hook (useCryptoMarkets)
2020
*/
21-
import { useEffect, useState } from 'react';
22-
import Loading from '../components/Loading.jsx';
23-
import ErrorMessage from '../components/ErrorMessage.jsx';
24-
import Card from '../components/Card.jsx';
25-
import formatNumber from '../utilities/numberFormatter.js';
21+
22+
import { useEffect, useState } from "react";
23+
import Loading from "../components/Loading.jsx";
24+
import ErrorMessage from "../components/ErrorMessage.jsx";
25+
import Card from "../components/Card.jsx";
26+
import formatNumber from "../utilities/numberFormatter.js";
27+
2628
export default function Crypto() {
2729
const [coins, setCoins] = useState([]);
28-
const [query, setQuery] = useState('');
30+
const [query, setQuery] = useState("");
2931
const [loading, setLoading] = useState(false);
3032
const [error, setError] = useState(null);
33+
const [page, setPage] = useState(1);
3134

32-
useEffect(() => { fetchCoins(); }, []);
35+
useEffect(() => {
36+
fetchCoins();
37+
}, [page]);
3338

3439
async function fetchCoins() {
3540
try {
36-
setLoading(true); setError(null);
37-
const res = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1&sparkline=false');
38-
if (!res.ok) throw new Error('Failed to fetch');
41+
setLoading(true);
42+
setError(null);
43+
const res = await fetch(
44+
`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=${page}&sparkline=false`
45+
);
46+
if (!res.ok) throw new Error("Failed to fetch");
3947
const json = await res.json();
4048
setCoins(json);
41-
} catch (e) { setError(e); } finally { setLoading(false); }
49+
} catch (e) {
50+
setError(e);
51+
} finally {
52+
setLoading(false);
53+
}
4254
}
4355

44-
const filtered = coins.filter(c => c.name.toLowerCase().includes(query.toLowerCase()));
56+
const filtered = coins.filter((c) =>
57+
c.name.toLowerCase().includes(query.toLowerCase())
58+
);
4559

4660
return (
4761
<div>
48-
<h2>Cryptocurrency Tracker</h2>
49-
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search coin" />
62+
<h2>💹 Cryptocurrency Tracker</h2>
63+
<input
64+
value={query}
65+
onChange={(e) => setQuery(e.target.value)}
66+
placeholder="Search coin..."
67+
style={{ marginBottom: "1rem" }}
68+
/>
69+
5070
{loading && <Loading />}
5171
<ErrorMessage error={error} />
72+
5273
<div className="grid">
53-
{filtered.map(c => (
54-
<Card key={c.id} title={c.name} footer={<span>${c.current_price}</span>}>
55-
<p>Market Cap: ${formatNumber(c.market_cap)}</p>
56-
<p>24h: {c.price_change_percentage_24h?.toFixed(2)}%</p>
57-
{/* TODO: Add mini sparkline chart */}
58-
</Card>
59-
))}
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+
})}
105+
</div>
106+
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+
>
117+
<button onClick={() => setPage((p) => p - 1)} disabled={page === 1}>
118+
Previous
119+
</button>
120+
121+
<span>Page {page}</span>
122+
123+
<button onClick={() => setPage((p) => p + 1)}>Next</button>
60124
</div>
61125
</div>
62126
);

src/styles.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,35 @@ blockquote {
412412
@media (max-width: 640px) {
413413
.weather-inner { padding: 1rem; }
414414
}
415+
416+
417+
/**Pagination **/
418+
419+
.pagination {
420+
width: 100%;
421+
display: flex;
422+
justify-content: center;
423+
align-items: center;
424+
margin-top: 20px;
425+
gap: 15px;
426+
}
427+
428+
.pagination button {
429+
padding: 8px 16px;
430+
border-radius: 5px;
431+
border: none;
432+
background-color: #007bff;
433+
color: white;
434+
font-weight: bold;
435+
cursor: pointer;
436+
transition: background-color 0.2s;
437+
}
438+
439+
.pagination button:hover {
440+
background-color: #0056b3;
441+
}
442+
443+
.pagination button:disabled {
444+
background-color: #ccc;
445+
cursor: not-allowed;
446+
}

0 commit comments

Comments
 (0)