Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
157 changes: 157 additions & 0 deletions src/components/CryptoCalculatorModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { useState, useEffect, useRef } from 'react';

export default function CryptoCalculatorModal({ coinData, onClose }) {
const [amount, setAmount] = useState('');
const [selectedCoinId, setSelectedCoinId] = useState(coinData[0]?.id || '');

const [searchTerm, setSearchTerm] = useState('');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef(null);

const [result, setResult] = useState(null);

useEffect(() => {
setResult(null);
}, [amount, selectedCoinId]);

useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsDropdownOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [dropdownRef]);


const isButtonDisabled = !amount || parseFloat(amount) <= 0;

const handleCalculate = () => {
const coin = coinData.find((c) => c.id === selectedCoinId);
const inputAmount = parseFloat(amount);
if (!coin) return;

const changePercent = coin.price_change_percentage_24h;
const changeDecimal = changePercent / 100;
const profitOrLoss = inputAmount * changeDecimal;
const finalAmount = inputAmount + profitOrLoss;

setResult({
finalAmount: finalAmount.toFixed(2),
profitOrLoss: profitOrLoss.toFixed(2),
isProfit: profitOrLoss >= 0,
error: null,
});
};

const filteredCoins = coinData.filter(coin =>
coin.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
coin.symbol.toLowerCase().includes(searchTerm.toLowerCase())
);

const selectedCoin = coinData.find(c => c.id === selectedCoinId);

return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<button className="modal-close-button" onClick={onClose}>
&times;
</button>
<h2>24h Profit/Loss Calculator</h2>

<div className="modal-input-group">
<label htmlFor="amount">Amount (USD)</label>
<input
id="amount"
type="number"
min="0"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="e.g., 100"
className="modal-input"
/>
</div>

<div className="modal-input-group">
<label htmlFor="crypto-search">Cryptocurrency</label>
<div className="modal-dropdown-container" ref={dropdownRef}>
<input
id="crypto-search"
type="text"
className="modal-input"
value={isDropdownOpen ? searchTerm : (selectedCoin?.name || '')}
onChange={(e) => {
setSearchTerm(e.target.value);
setIsDropdownOpen(true);
}}
onFocus={() => setIsDropdownOpen(true)}
placeholder="Search coin..."
/>
{isDropdownOpen && (
<div className="modal-dropdown-list">
{filteredCoins.length > 0 ? (
filteredCoins.map((coin) => (
<div
key={coin.id}
className="modal-dropdown-item"
onClick={() => {
setSelectedCoinId(coin.id);
setSearchTerm('');
setIsDropdownOpen(false);
}}
>
<img src={coin.image} alt={coin.symbol} style={{ width: 20, height: 20 }}/>
{coin.name} ({coin.symbol.toUpperCase()})
</div>
))
) : (
<div className="modal-dropdown-item-none">
No results found
</div>
)}
</div>
)}
</div>
</div>

<button
onClick={handleCalculate}
className="modal-button"
disabled={isButtonDisabled}
>
Calculate
</button>

{result && (
<div className="modal-result">
{result.error ? (
<p style={{ color: 'var(--danger)' }}>{result.error}</p>
) : (
<>
<p>Your ${amount} invested 24h ago would now be worth:</p>
<h3
style={{
color: result.isProfit ? '#16a34a' : 'var(--danger)',
fontSize: '1.5rem',
margin: '0.5rem 0',
}}
>
${result.finalAmount}
</h3>
<p>
A {result.isProfit ? 'profit' : 'loss'} of{' '}
<strong>
{result.isProfit ? '+' : ''}${result.profitOrLoss}
</strong>
</p>
</>
)}
</div>
)}
</div>
</div>
);
}
28 changes: 27 additions & 1 deletion src/pages/Crypto.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import Card from "../components/Card.jsx";
import formatNumber from "../utilities/numberFormatter.js";
import HeroSection from '../components/HeroSection';
import CryptoImg from '../Images/Cryptocurrency.jpg';

import CryptoCalculatorModal from "../components/CryptoCalculatorModal.jsx";

export default function Crypto() {
const [coins, setCoins] = useState([]);
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [isCalculatorOpen, setIsCalculatorOpen] = useState(false);

useEffect(() => {
fetchCoins();
Expand Down Expand Up @@ -79,6 +80,23 @@ export default function Crypto() {
style={{ marginBottom: "1rem" }}
/>

<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem', alignItems: 'center' }}>
<button
onClick={() => setIsCalculatorOpen(true)}
style={{
padding: '0.5rem 1rem',
cursor: 'pointer',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
whiteSpace: 'nowrap'
}}
>
24h Calculator
</button>
</div>

{loading && <Loading />}
<ErrorMessage error={error} />

Expand Down Expand Up @@ -134,6 +152,14 @@ export default function Crypto() {

<button onClick={() => setPage((p) => p + 1)}>Next</button>
</div>

{isCalculatorOpen && coins.length > 0 && (
<CryptoCalculatorModal
coinData={coins}
onClose={() => setIsCalculatorOpen(false)}
/>
)}

</div>
);
}
134 changes: 134 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -922,4 +922,138 @@ blockquote {
}
}

.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.2s ease-out;
}

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

.modal {
background-color: var(--bg);
color: var(--text);
padding: 2rem;
border-radius: var(--radius);
width: 90%;
max-width: 500px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
position: relative;
animation: modalSlideIn 0.3s ease-out;
}

@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

.modal-close-button {
position: absolute;
top: 1rem;
right: 1rem;
background: transparent;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text);
opacity: 0.7;
transition: opacity 0.2s ease;
}

.modal-close-button:hover {
opacity: 1;
}

.modal-input-group {
margin-bottom: 1rem;
}

.modal-input-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}

.modal-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-alt);
color: var(--text);
box-sizing: border-box;
}

.modal-button {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
}

.modal-result {
margin-top: 1.5rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background-color: var(--bg-alt);
text-align: center;
}

.modal-dropdown-container {
position: relative;
}

.modal-dropdown-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--bg);
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 var(--radius) var(--radius);
max-height: 150px;
overflow-y: auto;
z-index: 1001;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.modal-dropdown-item,
.modal-dropdown-item-none {
padding: 0.75rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}

.modal-dropdown-item:hover {
background-color: var(--bg-alt);
}

.modal-dropdown-item-none {
font-style: italic;
opacity: 0.7;
cursor: default;
}