Skip to content

Commit 6600024

Browse files
Merge pull request #99 from meetdhorajiya/feature/add-24h-crypto-calculator
feat: Add 24h profit/loss calculator modal
2 parents 8ba2f01 + 273f2d7 commit 6600024

File tree

3 files changed

+318
-1
lines changed

3 files changed

+318
-1
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
3+
export default function CryptoCalculatorModal({ coinData, onClose }) {
4+
const [amount, setAmount] = useState('');
5+
const [selectedCoinId, setSelectedCoinId] = useState(coinData[0]?.id || '');
6+
7+
const [searchTerm, setSearchTerm] = useState('');
8+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
9+
const dropdownRef = useRef(null);
10+
11+
const [result, setResult] = useState(null);
12+
13+
useEffect(() => {
14+
setResult(null);
15+
}, [amount, selectedCoinId]);
16+
17+
useEffect(() => {
18+
function handleClickOutside(event) {
19+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
20+
setIsDropdownOpen(false);
21+
}
22+
}
23+
document.addEventListener("mousedown", handleClickOutside);
24+
return () => {
25+
document.removeEventListener("mousedown", handleClickOutside);
26+
};
27+
}, [dropdownRef]);
28+
29+
30+
const isButtonDisabled = !amount || parseFloat(amount) <= 0;
31+
32+
const handleCalculate = () => {
33+
const coin = coinData.find((c) => c.id === selectedCoinId);
34+
const inputAmount = parseFloat(amount);
35+
if (!coin) return;
36+
37+
const changePercent = coin.price_change_percentage_24h;
38+
const changeDecimal = changePercent / 100;
39+
const profitOrLoss = inputAmount * changeDecimal;
40+
const finalAmount = inputAmount + profitOrLoss;
41+
42+
setResult({
43+
finalAmount: finalAmount.toFixed(2),
44+
profitOrLoss: profitOrLoss.toFixed(2),
45+
isProfit: profitOrLoss >= 0,
46+
error: null,
47+
});
48+
};
49+
50+
const filteredCoins = coinData.filter(coin =>
51+
coin.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
52+
coin.symbol.toLowerCase().includes(searchTerm.toLowerCase())
53+
);
54+
55+
const selectedCoin = coinData.find(c => c.id === selectedCoinId);
56+
57+
return (
58+
<div className="modal-overlay" onClick={onClose}>
59+
<div className="modal" onClick={(e) => e.stopPropagation()}>
60+
<button className="modal-close-button" onClick={onClose}>
61+
&times;
62+
</button>
63+
<h2>24h Profit/Loss Calculator</h2>
64+
65+
<div className="modal-input-group">
66+
<label htmlFor="amount">Amount (USD)</label>
67+
<input
68+
id="amount"
69+
type="number"
70+
min="0"
71+
value={amount}
72+
onChange={(e) => setAmount(e.target.value)}
73+
placeholder="e.g., 100"
74+
className="modal-input"
75+
/>
76+
</div>
77+
78+
<div className="modal-input-group">
79+
<label htmlFor="crypto-search">Cryptocurrency</label>
80+
<div className="modal-dropdown-container" ref={dropdownRef}>
81+
<input
82+
id="crypto-search"
83+
type="text"
84+
className="modal-input"
85+
value={isDropdownOpen ? searchTerm : (selectedCoin?.name || '')}
86+
onChange={(e) => {
87+
setSearchTerm(e.target.value);
88+
setIsDropdownOpen(true);
89+
}}
90+
onFocus={() => setIsDropdownOpen(true)}
91+
placeholder="Search coin..."
92+
/>
93+
{isDropdownOpen && (
94+
<div className="modal-dropdown-list">
95+
{filteredCoins.length > 0 ? (
96+
filteredCoins.map((coin) => (
97+
<div
98+
key={coin.id}
99+
className="modal-dropdown-item"
100+
onClick={() => {
101+
setSelectedCoinId(coin.id);
102+
setSearchTerm('');
103+
setIsDropdownOpen(false);
104+
}}
105+
>
106+
<img src={coin.image} alt={coin.symbol} style={{ width: 20, height: 20 }}/>
107+
{coin.name} ({coin.symbol.toUpperCase()})
108+
</div>
109+
))
110+
) : (
111+
<div className="modal-dropdown-item-none">
112+
No results found
113+
</div>
114+
)}
115+
</div>
116+
)}
117+
</div>
118+
</div>
119+
120+
<button
121+
onClick={handleCalculate}
122+
className="modal-button"
123+
disabled={isButtonDisabled}
124+
>
125+
Calculate
126+
</button>
127+
128+
{result && (
129+
<div className="modal-result">
130+
{result.error ? (
131+
<p style={{ color: 'var(--danger)' }}>{result.error}</p>
132+
) : (
133+
<>
134+
<p>Your ${amount} invested 24h ago would now be worth:</p>
135+
<h3
136+
style={{
137+
color: result.isProfit ? '#16a34a' : 'var(--danger)',
138+
fontSize: '1.5rem',
139+
margin: '0.5rem 0',
140+
}}
141+
>
142+
${result.finalAmount}
143+
</h3>
144+
<p>
145+
A {result.isProfit ? 'profit' : 'loss'} of{' '}
146+
<strong>
147+
{result.isProfit ? '+' : ''}${result.profitOrLoss}
148+
</strong>
149+
</p>
150+
</>
151+
)}
152+
</div>
153+
)}
154+
</div>
155+
</div>
156+
);
157+
}

src/pages/Crypto.jsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ import Card from "../components/Card.jsx";
2626
import formatNumber from "../utilities/numberFormatter.js";
2727
import HeroSection from '../components/HeroSection';
2828
import CryptoImg from '../Images/Cryptocurrency.jpg';
29-
29+
import CryptoCalculatorModal from "../components/CryptoCalculatorModal.jsx";
3030

3131
export default function Crypto() {
3232
const [coins, setCoins] = useState([]);
3333
const [query, setQuery] = useState("");
3434
const [loading, setLoading] = useState(false);
3535
const [error, setError] = useState(null);
3636
const [page, setPage] = useState(1);
37+
const [isCalculatorOpen, setIsCalculatorOpen] = useState(false);
3738

3839
useEffect(() => {
3940
fetchCoins();
@@ -79,6 +80,23 @@ export default function Crypto() {
7980
style={{ marginBottom: "1rem" }}
8081
/>
8182

83+
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem', alignItems: 'center' }}>
84+
<button
85+
onClick={() => setIsCalculatorOpen(true)}
86+
style={{
87+
padding: '0.5rem 1rem',
88+
cursor: 'pointer',
89+
backgroundColor: '#007bff',
90+
color: 'white',
91+
border: 'none',
92+
borderRadius: '4px',
93+
whiteSpace: 'nowrap'
94+
}}
95+
>
96+
24h Calculator
97+
</button>
98+
</div>
99+
82100
{loading && <Loading />}
83101
<ErrorMessage error={error} />
84102

@@ -134,6 +152,14 @@ export default function Crypto() {
134152

135153
<button onClick={() => setPage((p) => p + 1)}>Next</button>
136154
</div>
155+
156+
{isCalculatorOpen && coins.length > 0 && (
157+
<CryptoCalculatorModal
158+
coinData={coins}
159+
onClose={() => setIsCalculatorOpen(false)}
160+
/>
161+
)}
162+
137163
</div>
138164
);
139165
}

src/styles.css

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,4 +922,138 @@ blockquote {
922922
}
923923
}
924924

925+
.modal-overlay {
926+
position: fixed;
927+
top: 0;
928+
left: 0;
929+
right: 0;
930+
bottom: 0;
931+
background-color: rgba(0, 0, 0, 0.7);
932+
display: flex;
933+
justify-content: center;
934+
align-items: center;
935+
z-index: 1000;
936+
animation: fadeIn 0.2s ease-out;
937+
}
938+
939+
@keyframes fadeIn {
940+
from {
941+
opacity: 0;
942+
}
943+
to {
944+
opacity: 1;
945+
}
946+
}
947+
948+
.modal {
949+
background-color: var(--bg);
950+
color: var(--text);
951+
padding: 2rem;
952+
border-radius: var(--radius);
953+
width: 90%;
954+
max-width: 500px;
955+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
956+
position: relative;
957+
animation: modalSlideIn 0.3s ease-out;
958+
}
959+
960+
@keyframes modalSlideIn {
961+
from {
962+
opacity: 0;
963+
transform: translateY(-20px) scale(0.95);
964+
}
965+
to {
966+
opacity: 1;
967+
transform: translateY(0) scale(1);
968+
}
969+
}
970+
971+
.modal-close-button {
972+
position: absolute;
973+
top: 1rem;
974+
right: 1rem;
975+
background: transparent;
976+
border: none;
977+
font-size: 1.5rem;
978+
cursor: pointer;
979+
color: var(--text);
980+
opacity: 0.7;
981+
transition: opacity 0.2s ease;
982+
}
983+
984+
.modal-close-button:hover {
985+
opacity: 1;
986+
}
925987

988+
.modal-input-group {
989+
margin-bottom: 1rem;
990+
}
991+
992+
.modal-input-group label {
993+
display: block;
994+
margin-bottom: 0.5rem;
995+
font-weight: bold;
996+
}
997+
998+
.modal-input {
999+
width: 100%;
1000+
padding: 0.5rem 0.75rem;
1001+
border: 1px solid var(--border);
1002+
border-radius: var(--radius);
1003+
background: var(--bg-alt);
1004+
color: var(--text);
1005+
box-sizing: border-box;
1006+
}
1007+
1008+
.modal-button {
1009+
width: 100%;
1010+
padding: 0.75rem;
1011+
font-size: 1rem;
1012+
}
1013+
1014+
.modal-result {
1015+
margin-top: 1.5rem;
1016+
padding: 1rem;
1017+
border: 1px solid var(--border);
1018+
border-radius: var(--radius);
1019+
background-color: var(--bg-alt);
1020+
text-align: center;
1021+
}
1022+
1023+
.modal-dropdown-container {
1024+
position: relative;
1025+
}
1026+
1027+
.modal-dropdown-list {
1028+
position: absolute;
1029+
top: 100%;
1030+
left: 0;
1031+
right: 0;
1032+
background-color: var(--bg);
1033+
border: 1px solid var(--border);
1034+
border-top: none;
1035+
border-radius: 0 0 var(--radius) var(--radius);
1036+
max-height: 150px;
1037+
overflow-y: auto;
1038+
z-index: 1001;
1039+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1040+
}
1041+
1042+
.modal-dropdown-item,
1043+
.modal-dropdown-item-none {
1044+
padding: 0.75rem;
1045+
cursor: pointer;
1046+
display: flex;
1047+
align-items: center;
1048+
gap: 8px;
1049+
}
1050+
1051+
.modal-dropdown-item:hover {
1052+
background-color: var(--bg-alt);
1053+
}
1054+
1055+
.modal-dropdown-item-none {
1056+
font-style: italic;
1057+
opacity: 0.7;
1058+
cursor: default;
1059+
}

0 commit comments

Comments
 (0)