-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #393 from Ibinola/feat/add-history-page
feat: add history page, implement usePosition hook to fetch table data
- Loading branch information
Showing
9 changed files
with
512 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { axiosInstance } from 'utils/axios'; | ||
import { formatDate } from 'utils/formatDate'; | ||
import { useWalletStore } from 'stores/useWalletStore'; | ||
|
||
const fetchPositionHistoryTable = async (walletId) => { | ||
if (!walletId) { | ||
throw new Error('Wallet ID is undefined'); | ||
} | ||
const response = await axiosInstance.get(`/api/user-positions/${walletId}`); | ||
return response.data; | ||
}; | ||
|
||
const formatPositionHistoryTable = (data) => { | ||
return data.map((position) => ({ | ||
...position, | ||
amount: Number(position.amount).toFixed(2), | ||
start_price: `$${position.start_price.toFixed(2)}`, | ||
multiplier: position.multiplier.toFixed(1), | ||
created_at: formatDate(position.created_at), | ||
datetime_liquidation: formatDate(position.datetime_liquidation), | ||
status: position.status.charAt(0).toUpperCase() + position.status.slice(1), | ||
is_liquidated: position.is_liquidated ? 'Yes' : 'No', | ||
})); | ||
}; | ||
|
||
const usePositionHistoryTable = () => { | ||
const walletId = useWalletStore((state) => state.walletId); | ||
|
||
const { data, isPending, error } = useQuery({ | ||
queryKey: ['positionHistory', walletId], | ||
queryFn: () => fetchPositionHistoryTable(walletId), | ||
enabled: !!walletId, | ||
onError: (err) => { | ||
console.error('Error during fetching position history:', err); | ||
}, | ||
select: formatPositionHistoryTable, | ||
}); | ||
|
||
const showSpinner = !!walletId && isPending; | ||
|
||
return { | ||
data, | ||
isPending: showSpinner, | ||
error: walletId ? error : 'Wallet ID is required', | ||
}; | ||
}; | ||
|
||
export { usePositionHistoryTable }; |
127 changes: 127 additions & 0 deletions
127
frontend/src/pages/spotnet/position_history/PositionHistory.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React, { useState } from 'react'; | ||
import './positionHistory.css'; | ||
import { ReactComponent as HealthIcon } from 'assets/icons/health.svg'; | ||
import { ReactComponent as EthIcon } from 'assets/icons/ethereum.svg'; | ||
import { ReactComponent as StrkIcon } from 'assets/icons/strk.svg'; | ||
import { ReactComponent as UsdIcon } from 'assets/icons/usd_coin.svg'; | ||
import { usePositionHistoryTable } from 'hooks/usePositionHistory'; | ||
import Spinner from 'components/spinner/Spinner'; | ||
import filterIcon from '../../../assets/icons/filter-horizontal.svg'; | ||
import useDashboardData from 'hooks/useDashboardData'; | ||
import Card from 'components/Card/Card'; | ||
import PositionHistoryModal from 'pages/spotnet/position_history/PositionHistoryModal'; | ||
|
||
function PositionHistory() { | ||
const [selectedPosition, setSelectedPosition] = useState(null); | ||
|
||
const { data: tableData, isPending } = usePositionHistoryTable(); | ||
const { data: cardData } = useDashboardData(); | ||
|
||
const tokenIconMap = { | ||
STRK: <StrkIcon className="token-icon" />, | ||
USDC: <UsdIcon className="token-icon" />, | ||
ETH: <EthIcon className="token-icon" />, | ||
}; | ||
|
||
const statusStyles = { | ||
opened: 'status-opened', | ||
closed: 'status-closed', | ||
pending: 'status-pending', | ||
}; | ||
|
||
return ( | ||
<div className="position-wrapper"> | ||
<div className="position-container"> | ||
<h1 className="position-title">zkLend Position History</h1> | ||
<div className="position-content"> | ||
<div className="position-top-cards"> | ||
<Card | ||
label="Health Factor" | ||
value={cardData?.health_ratio || '0.00'} | ||
icon={<HealthIcon className="icon" />} | ||
/> | ||
<Card label="Borrow Balance" cardData={cardData?.borrowed || '0.00'} icon={<EthIcon className="icon" />} /> | ||
</div> | ||
</div> | ||
|
||
<div className="position-content-table"> | ||
<div className="position-table-title"> | ||
<p>Position History</p> | ||
</div> | ||
|
||
<div className="position-table"> | ||
{isPending ? ( | ||
<div className="spinner-container"> | ||
<Spinner loading={isPending} /> | ||
</div> | ||
) : ( | ||
<table className="text-white"> | ||
<thead> | ||
<tr> | ||
<th></th> | ||
<th>Token</th> | ||
<th>Amount</th> | ||
<th>Created At</th> | ||
<th>Status</th> | ||
<th>Start Price</th> | ||
<th>Multiplier</th> | ||
<th>Liquidated</th> | ||
<th>Closed At</th> | ||
<th className="action-column"> | ||
<img src={filterIcon} alt="filter-icon" draggable="false" /> | ||
</th> | ||
</tr> | ||
</thead> | ||
|
||
<tbody> | ||
{!tableData || tableData.length === 0 ? ( | ||
<tr> | ||
<td colSpan="10">No opened positions</td> | ||
</tr> | ||
) : ( | ||
tableData.map((data, index) => ( | ||
<tr key={data.id}> | ||
<td className="index">{index + 1}.</td> | ||
<td> | ||
<div className="token-cell"> | ||
{tokenIconMap[data.token_symbol]} | ||
<span className="token-symbol">{data.token_symbol.toUpperCase()}</span> | ||
</div> | ||
</td> | ||
<td>{data.amount}</td> | ||
<td>{data.created_at}</td> | ||
<td className={`status-cell ${statusStyles[data.status.toLowerCase()] || ''}`}> | ||
{data.status} | ||
</td> | ||
<td>{data.start_price}</td> | ||
<td>{data.multiplier}</td> | ||
<td>{data.is_liquidated}</td> | ||
<td>{data.datetime_liquidation}</td> | ||
<td className="action-column"> | ||
<span className="action-button" onClick={() => setSelectedPosition({ data, index })}> | ||
⋮ | ||
</span> | ||
</td> | ||
</tr> | ||
)) | ||
)} | ||
</tbody> | ||
</table> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
{selectedPosition && ( | ||
<PositionHistoryModal | ||
position={selectedPosition.data} | ||
onClose={() => setSelectedPosition(null)} | ||
tokenIcon={tokenIconMap} | ||
statusStyles={statusStyles} | ||
index={selectedPosition.index + 1} | ||
/> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export default PositionHistory; |
58 changes: 58 additions & 0 deletions
58
frontend/src/pages/spotnet/position_history/PositionHistoryModal.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import React from 'react'; | ||
import './positionHistory.css'; | ||
|
||
function PositionHistoryModal({ position, onClose, index, tokenIcon, statusStyles }) { | ||
return ( | ||
<div className="position-modal-overlay"> | ||
<div className="position-modal-content"> | ||
<div className="position-modal-header"> | ||
<p className="position-modal-p"> | ||
<span> | ||
<span>{index}.</span> | ||
{tokenIcon[position.token_symbol]} | ||
{position.token_symbol} | ||
</span> | ||
<span>{position.amount}</span> | ||
<span className={`status-cell ${statusStyles[position.status.toLowerCase()] || ''}`}> | ||
{position.status} | ||
</span> | ||
</p> | ||
|
||
<span onClick={onClose} className="position-close-button" aria-label="Close Account Info Modal Box"> | ||
✕ | ||
</span> | ||
</div> | ||
<hr className="modal-divider" /> | ||
<div className="position-modal-body"> | ||
<div className="position-detail-row"> | ||
<p> | ||
Start Price <span>{position.start_price}</span> | ||
</p> | ||
</div> | ||
<div className="position-detail-row"> | ||
<p> | ||
Multiplier <span>{position.multiplier}</span> | ||
</p> | ||
</div> | ||
<div className="position-detail-row"> | ||
<p> | ||
Liquidated <span>{position.is_liquidated ? 'Yes' : 'No'}</span> | ||
</p> | ||
</div> | ||
<div className="position-detail-row"> | ||
<p> | ||
Created At <span>{position.created_at}</span> | ||
</p> | ||
</div> | ||
<div className="position-detail-row"> | ||
<p> | ||
Closed At <span>{position.datetime_liquidation}</span> | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default PositionHistoryModal; |
Oops, something went wrong.