Skip to content

Commit

Permalink
Merge pull request #309 from bounswe/feature/FE-portfolio-page
Browse files Browse the repository at this point in the history
Add portfolio page
  • Loading branch information
danzio19 authored Oct 21, 2024
2 parents 9e441ba + 6686f47 commit bef5bf7
Show file tree
Hide file tree
Showing 9 changed files with 1,085 additions and 8 deletions.
60 changes: 60 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-google-charts": "^5.1.0",
"react-router-dom": "^6.27.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
Expand Down
126 changes: 126 additions & 0 deletions frontend/src/components/portfolio/AssetList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React, { useState } from 'react';
import '../../styles/Portfolio.css';
import mockStocks from '../../data/mockStocks';

const AssetList = ({ assets, setAssets }) => {
const [editIndex, setEditIndex] = useState(null);
const [editData, setEditData] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;

const handleEdit = (index, field, value) => {
setEditData({ ...editData, [field]: value });
};

const saveEdit = (index) => {
const updatedAssets = [...assets];
updatedAssets[index] = { ...updatedAssets[index], ...editData };
setAssets(updatedAssets);
setEditIndex(null);
};

const handleDelete = (index) => {
if (window.confirm('Are you sure you want to delete this asset?')) {
const updatedAssets = assets.filter((_, i) => i !== index);
setAssets(updatedAssets);
}
};

const calculateProfitLoss = (asset) => {
const assetCode = asset.stockCode;
const assetPrice = mockStocks.find(stock => stock.code === assetCode)?.price || 0;
const profitLoss = (parseFloat(assetPrice) - parseFloat(asset.stockPrice)) * parseFloat(asset.quantity);
return profitLoss.toFixed(2);
};

const getProfitLossClass = (asset) => {
const profitLoss = parseFloat(calculateProfitLoss(asset));
if (profitLoss > 0) return 'green';
if (profitLoss < 0) return 'red';
return '';
};

const totalPages = Math.ceil(assets.length / itemsPerPage);
const displayedAssets = assets.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);

return (
<div className="asset-list-container">
<table className="asset-table">
<thead>
<tr>
<th>Stock Code</th>
<th>Stock Price</th>
<th>Quantity</th>
<th>Profit/Loss Margin</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{displayedAssets.map((asset, index) => (
<tr key={index}>
{editIndex === index ? (
<>
<td>
<input
type="text"
value={editData.stockCode || asset.stockCode}
onChange={(e) => handleEdit(index, 'stockCode', e.target.value)}
className="asset-input"
/>
</td>
<td>
<input
type="number"
value={editData.stockPrice || asset.stockPrice}
onChange={(e) => handleEdit(index, 'stockPrice', e.target.value)}
className="asset-input"
/>
</td>
<td>
<input
type="number"
value={editData.quantity || asset.quantity}
onChange={(e) => handleEdit(index, 'quantity', e.target.value)}
className="asset-input"
/>
</td>
<td className={getProfitLossClass(asset)}>
{calculateProfitLoss(asset)}
</td>
<td>
<button onClick={() => saveEdit(index)} className="asset-save-button">Save</button>
</td>
</>
) : (
<>
<td>{asset.stockCode}</td>
<td>{asset.stockPrice.toFixed(2)}</td>
<td>{asset.quantity}</td>
<td className={getProfitLossClass(asset)}>
{calculateProfitLoss(asset)}
</td>
<td>
<button onClick={() => setEditIndex(index)} className="asset-edit-button">Edit</button>
<button onClick={() => handleDelete(index)} className="asset-delete-button">Delete</button>
</td>
</>
)}
</tr>
))}
</tbody>
</table>

<div className="pagination-controls">
<button onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))} disabled={currentPage === 1}>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages}>
Next
</button>
</div>
</div>
);
};

export default AssetList;
80 changes: 80 additions & 0 deletions frontend/src/components/portfolio/AssetModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useState } from 'react';
import '../../styles/Portfolio.css';

const AssetModal = ({ onClose, onSubmit, stockData }) => {
const [stockCode, setStockCode] = useState('');
const [stockPrice, setStockPrice] = useState('');
const [quantity, setQuantity] = useState('');
const [searchResults, setSearchResults] = useState([]);

const handleStockSearch = (code) => {
setStockCode(code);
if (code) {
const results = stockData
.filter((s) => s.code.toLowerCase().includes(code.toLowerCase()))
.slice(0, 5);
setSearchResults(results);
} else {
setSearchResults([]);
}
};

const handleSelectStock = (stock) => {
setStockCode(stock.code);
setStockPrice(stock.price);
setSearchResults([]);
};

const handleSubmit = () => {
if (stockCode && stockPrice && quantity) {
onSubmit({ stockCode, stockPrice: parseFloat(stockPrice), quantity: parseInt(quantity) });
onClose();
}
};

return (
<div className="modal-overlay">
<div className="modal-content">
<h2>Add Asset</h2>
<input
type="text"
value={stockCode}
onChange={(e) => handleStockSearch(e.target.value)}
placeholder="Stock Code"
className="modal-input"
/>
<div className="search-results">
{searchResults.map((stock) => (
<div
key={stock.code}
className="search-result-item"
onClick={() => handleSelectStock(stock)}
>
{stock.code} - {stock.name}
</div>
))}
</div>
<input
type="number"
value={stockPrice}
onChange={(e) => setStockPrice(e.target.value)}
placeholder="Stock Price"
className="modal-input"
/>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
placeholder="Quantity"
className="modal-input"
/>
<div className="modal-buttons">
<button onClick={handleSubmit} className="modal-submit-button">Submit</button>
<button onClick={onClose} className="modal-cancel-button">Cancel</button>
</div>
</div>
</div>
);
};

export default AssetModal;
36 changes: 36 additions & 0 deletions frontend/src/components/portfolio/PortfolioDetailsCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowUp, faArrowDown, faClipboardList, faDollarSign } from '@fortawesome/free-solid-svg-icons';
import '../../styles/Portfolio.css';

const PortfolioDetailsCard = ({ numAssets, totalValue, totalProfit }) => {
return (
<div className="portfolio-details-card">
<h3>Portfolio Details</h3>
<div className="portfolio-detail">
<FontAwesomeIcon icon={faClipboardList} className="detail-icon" />
<span className='detail-item'>Number of Assets:</span>
<span className="detail-value">{numAssets}</span>
</div>
<div className="portfolio-detail">
<FontAwesomeIcon icon={faDollarSign} className="detail-icon" />
<span className='detail-item'>Total Value:</span>
<span className="detail-value">${totalValue.toFixed(2)}</span>
</div>
<div className="profit-section portfolio-detail">
<span className='detail-item'>Total Profit:</span>
<span className={`detail-value ${totalProfit >= 0 ? 'profit' : 'loss'}`}>
${totalProfit.toFixed(2)}
{totalProfit >= 0 ? (
<FontAwesomeIcon icon={faArrowUp} className="detail-icon" />
) : (
<FontAwesomeIcon icon={faArrowDown} className="detail-icon" />
)}
</span>

</div>
</div>
);
};

export default PortfolioDetailsCard;
34 changes: 34 additions & 0 deletions frontend/src/components/portfolio/PortfolioModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import '../../styles/Portfolio.css';

const PortfolioModal = ({ onClose, onSubmit }) => {
const [portfolioName, setPortfolioName] = useState('');

const handleSubmit = () => {
if (portfolioName) {
onSubmit(portfolioName);
onClose();
}
};

return (
<div className="modal-overlay">
<div className="modal-content">
<h2>Create Portfolio</h2>
<input
type="text"
value={portfolioName}
onChange={(e) => setPortfolioName(e.target.value)}
placeholder="Portfolio Name"
className="modal-input"
/>
<div className="modal-buttons">
<button onClick={handleSubmit} className="modal-submit-button">Submit</button>
<button onClick={onClose} className="modal-cancel-button">Cancel</button>
</div>
</div>
</div>
);
};

export default PortfolioModal;
Loading

0 comments on commit bef5bf7

Please sign in to comment.