cryptostock-pro / frontend /src /components /AddHoldingModal.jsx
Ubuntu
Initial commit: CryptoStock Pro - Full Stack Portfolio Tracker
2a60e5d
Raw
History Blame Contribute Delete
7.68 kB
import React, { useState } from 'react';
import { X, Search, Bitcoin, BarChart3 } from 'lucide-react';
import { marketAPI } from '../utils/api';
const AddHoldingModal = ({ isOpen, onClose, onAdd }) => {
const [formData, setFormData] = useState({
symbol: '',
name: '',
asset_type: 'stock',
quantity: '',
buy_price: '',
buy_date: new Date().toISOString().split('T')[0],
notes: ''
});
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const handleSearch = async (query) => {
if (query.length < 1) {
setSearchResults([]);
return;
}
setIsSearching(true);
try {
const response = await marketAPI.search(query, formData.asset_type);
setSearchResults(response.data.slice(0, 5));
} catch (err) {
console.error('Search error:', err);
} finally {
setIsSearching(false);
}
};
const selectAsset = (asset) => {
setFormData({
...formData,
symbol: asset.symbol,
name: asset.name
});
setSearchResults([]);
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (!formData.symbol || !formData.quantity || !formData.buy_price) {
setError('Please fill in all required fields');
return;
}
setIsSubmitting(true);
try {
const success = await onAdd({
...formData,
quantity: parseFloat(formData.quantity),
buy_price: parseFloat(formData.buy_price),
buy_date: new Date(formData.buy_date).toISOString()
});
if (success) {
onClose();
setFormData({
symbol: '',
name: '',
asset_type: 'stock',
quantity: '',
buy_price: '',
buy_date: new Date().toISOString().split('T')[0],
notes: ''
});
} else {
setError('Failed to add holding');
}
} catch (err) {
setError(err.message);
} finally {
setIsSubmitting(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
<div className="glass-card w-full max-w-md animate-fade-in">
<div className="flex items-center justify-between p-6 border-b border-white/10">
<h2 className="text-xl font-bold">Add Holding</h2>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
{/* Asset Type Toggle */}
<div className="flex gap-2">
<button
type="button"
onClick={() => setFormData({ ...formData, asset_type: 'stock' })}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl border transition-all ${
formData.asset_type === 'stock'
? 'bg-primary-500/20 border-primary-500 text-primary-400'
: 'border-white/10 text-gray-400 hover:border-white/20'
}`}
>
<BarChart3 className="w-5 h-5" />
<span>Stock</span>
</button>
<button
type="button"
onClick={() => setFormData({ ...formData, asset_type: 'crypto' })}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl border transition-all ${
formData.asset_type === 'crypto'
? 'bg-orange-500/20 border-orange-500 text-orange-400'
: 'border-white/10 text-gray-400 hover:border-white/20'
}`}
>
<Bitcoin className="w-5 h-5" />
<span>Crypto</span>
</button>
</div>
{/* Symbol Search */}
<div className="relative">
<label className="block text-sm text-gray-400 mb-2">Symbol *</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
value={formData.symbol}
onChange={(e) => {
setFormData({ ...formData, symbol: e.target.value.toUpperCase() });
handleSearch(e.target.value);
}}
placeholder="Search symbol..."
className="input-field pl-10"
/>
</div>
{/* Search Results Dropdown */}
{searchResults.length > 0 && (
<div className="absolute w-full mt-2 glass-card py-2 z-10">
{searchResults.map((result) => (
<button
key={result.symbol}
type="button"
onClick={() => selectAsset(result)}
className="w-full px-4 py-2 text-left hover:bg-white/10 transition-colors"
>
<span className="font-medium">{result.symbol}</span>
<span className="text-gray-400 ml-2">{result.name}</span>
</button>
))}
</div>
)}
</div>
{/* Quantity & Price */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-400 mb-2">Quantity *</label>
<input
type="number"
step="any"
value={formData.quantity}
onChange={(e) => setFormData({ ...formData, quantity: e.target.value })}
placeholder="0.00"
className="input-field"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">Buy Price (USD) *</label>
<input
type="number"
step="any"
value={formData.buy_price}
onChange={(e) => setFormData({ ...formData, buy_price: e.target.value })}
placeholder="0.00"
className="input-field"
/>
</div>
</div>
{/* Buy Date */}
<div>
<label className="block text-sm text-gray-400 mb-2">Buy Date</label>
<input
type="date"
value={formData.buy_date}
onChange={(e) => setFormData({ ...formData, buy_date: e.target.value })}
className="input-field"
/>
</div>
{/* Notes */}
<div>
<label className="block text-sm text-gray-400 mb-2">Notes (optional)</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
placeholder="Add notes..."
rows={2}
className="input-field resize-none"
/>
</div>
{error && (
<p className="text-danger-400 text-sm">{error}</p>
)}
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="btn-primary w-full disabled:opacity-50"
>
{isSubmitting ? 'Adding...' : 'Add Holding'}
</button>
</form>
</div>
</div>
);
};
export default AddHoldingModal;