From 3b703bf1ca2f412ac2fdc57137cf39ba2e1a5726 Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Tue, 4 Jun 2024 00:33:55 -0700 Subject: [PATCH] purchase history table (#134) * purchase history table * small adjustments * handle no data --------- Co-authored-by: Sergej --- .env.example | 2 + next.config.js | 2 + src/components/Charts/SalePrice/index.tsx | 2 +- .../Elements/Buttons/ActionButton/index.tsx | 8 +- src/components/Elements/Link/index.tsx | 17 ++++ src/components/Elements/index.ts | 1 + .../Regions/PurchaseHistory/index.module.scss | 10 ++ .../Modals/Regions/PurchaseHistory/index.tsx | 93 ++++++++++++++++++ src/components/Modals/Regions/index.ts | 1 + .../DetailCard/index.module.scss | 2 +- .../Panels/SalePhaseInfoPanel/index.tsx | 81 ++++++++-------- src/components/Paras/index.ts | 1 - .../ParachainTable/index.module.scss | 0 .../ParachainTable/index.tsx | 45 ++------- .../Tables/PurchaseHistoryTable/index.tsx | 97 +++++++++++++++++++ src/components/Tables/common.ts | 24 +++++ src/components/Tables/index.ts | 3 + src/components/index.ts | 1 + src/consts/index.ts | 11 +++ src/hooks/index.ts | 1 + src/hooks/purchaseHistory.ts | 81 ++++++++++++++++ src/models/types.ts | 31 ++++++ src/pages/purchase.tsx | 31 +++++- 23 files changed, 461 insertions(+), 84 deletions(-) create mode 100644 src/components/Elements/Link/index.tsx create mode 100644 src/components/Modals/Regions/PurchaseHistory/index.module.scss create mode 100644 src/components/Modals/Regions/PurchaseHistory/index.tsx rename src/components/{Paras => Tables}/ParachainTable/index.module.scss (100%) rename src/components/{Paras => Tables}/ParachainTable/index.tsx (88%) create mode 100644 src/components/Tables/PurchaseHistoryTable/index.tsx create mode 100644 src/components/Tables/common.ts create mode 100644 src/components/Tables/index.ts create mode 100644 src/consts/index.ts create mode 100644 src/hooks/purchaseHistory.ts diff --git a/.env.example b/.env.example index 8f8f8543..a495c270 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,6 @@ WS_KUSAMA_CORETIME_CHAIN="WSS endpoint of the coretime chain" WS_ROCOCO_RELAY_CHAIN="WSS endpoint of the coretime relay chain" WS_KUSAMA_RELAY_CHAIN="WSS endpoint of the coretime relay chain" WS_REGIONX_CHAIN="WSS endpoint of the regionx chain" +ROCOCO_CORETIME_API="API endpoint for Rococo Coretime" +KUSAMA_CORETIME_API="API endpoint for Kusama Coretime" EXPERIMENTAL=false diff --git a/next.config.js b/next.config.js index b4e24b4e..149b9451 100644 --- a/next.config.js +++ b/next.config.js @@ -10,6 +10,8 @@ const nextConfig = { WS_REGIONX_CHAIN: process.env.WS_REGIONX_CHAIN || '', WS_ROCOCO_RELAY_CHAIN: process.env.WS_ROCOCO_RELAY_CHAIN, WS_KUSAMA_RELAY_CHAIN: process.env.WS_KUSAMA_RELAY_CHAIN, + KUSAMA_CORETIME_API: process.env.KUSAMA_CORETIME_API, + ROCOCO_CORETIME_API: process.env.ROCOCO_CORETIME_API, EXPERIMENTAL: process.env.EXPERIMENTAL, }, }; diff --git a/src/components/Charts/SalePrice/index.tsx b/src/components/Charts/SalePrice/index.tsx index 764ce7a0..33fa6149 100644 --- a/src/components/Charts/SalePrice/index.tsx +++ b/src/components/Charts/SalePrice/index.tsx @@ -106,7 +106,7 @@ export const SalePriceChart = () => { yaxis: { min: 0, title: { - text: `Price (${symbol})`, + text: symbol ? `Price (${symbol})` : 'Price', }, labels: { formatter: (v: number) => v?.toFixed(2), diff --git a/src/components/Elements/Buttons/ActionButton/index.tsx b/src/components/Elements/Buttons/ActionButton/index.tsx index f38eb933..c869223f 100644 --- a/src/components/Elements/Buttons/ActionButton/index.tsx +++ b/src/components/Elements/Buttons/ActionButton/index.tsx @@ -5,13 +5,19 @@ import styles from './index.module.scss'; interface ActionButtonProps { label: string; onClick: () => void; + disabled?: boolean; } -export const ActionButton = ({ label, onClick }: ActionButtonProps) => { +export const ActionButton = ({ + label, + onClick, + disabled, +}: ActionButtonProps) => { return ( diff --git a/src/components/Elements/Link/index.tsx b/src/components/Elements/Link/index.tsx new file mode 100644 index 00000000..7d0262f6 --- /dev/null +++ b/src/components/Elements/Link/index.tsx @@ -0,0 +1,17 @@ +import { Button } from '@mui/material'; +import React from 'react'; + +interface LinkProps { + href: string; + target?: string; + children: React.ReactNode; +} + +export const Link = ({ href, target = '_blank', children }: LinkProps) => { + const onClick = () => { + if (!href) return; + window.open(href, target); + }; + + return ; +}; diff --git a/src/components/Elements/index.ts b/src/components/Elements/index.ts index 453f69ef..a263bde2 100644 --- a/src/components/Elements/index.ts +++ b/src/components/Elements/index.ts @@ -6,6 +6,7 @@ export * from './Cards'; export * from './CountDown'; export * from './Inputs'; export * from './Label'; +export * from './Link'; export * from './MarketFilters'; export * from './Selectors'; export * from './StatusIndicator'; diff --git a/src/components/Modals/Regions/PurchaseHistory/index.module.scss b/src/components/Modals/Regions/PurchaseHistory/index.module.scss new file mode 100644 index 00000000..bdf05cdf --- /dev/null +++ b/src/components/Modals/Regions/PurchaseHistory/index.module.scss @@ -0,0 +1,10 @@ +.container { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 55rem; +} + +.tableContainer { + overflow-y: auto; +} \ No newline at end of file diff --git a/src/components/Modals/Regions/PurchaseHistory/index.tsx b/src/components/Modals/Regions/PurchaseHistory/index.tsx new file mode 100644 index 00000000..1c305f4a --- /dev/null +++ b/src/components/Modals/Regions/PurchaseHistory/index.tsx @@ -0,0 +1,93 @@ +import { Warning } from '@mui/icons-material'; +import { + Box, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + Stack, + Typography, + useTheme, +} from '@mui/material'; +import React from 'react'; + +import { usePurchaseHistory } from '@/hooks'; + +import { ActionButton } from '@/components/Elements'; +import { PurchaseHistoryTable } from '@/components/Tables'; + +import { useNetwork } from '@/contexts/network'; +import { useSaleInfo } from '@/contexts/sales'; + +import styles from './index.module.scss'; + +interface PurchaseHistoryModalProps { + open: boolean; + onClose: () => void; +} + +export const PurchaseHistoryModal = ({ + open, + onClose, +}: PurchaseHistoryModalProps) => { + const theme = useTheme(); + const { network } = useNetwork(); + const { + saleInfo: { regionBegin }, + } = useSaleInfo(); + const { loading, data, isError } = usePurchaseHistory( + network, + regionBegin, + 0, + 20 + ); + + return ( + + + + + Purchase History + + + Get an insight into all purchases and renewals made during the + current bulk period. + + + + {loading || isError ? ( + + {loading ? ( + + ) : ( + + + Failed to fetch purchase history + + )} + + ) : ( + + + + )} + + + + + + + + + ); +}; diff --git a/src/components/Modals/Regions/index.ts b/src/components/Modals/Regions/index.ts index 2325187e..13184b71 100644 --- a/src/components/Modals/Regions/index.ts +++ b/src/components/Modals/Regions/index.ts @@ -4,6 +4,7 @@ export * from './Partition'; export * from './Pooling'; export * from './Price'; export * from './Purchase'; +export * from './PurchaseHistory'; export * from './Sell'; export * from './TaskAssign'; export * from './Transfer'; diff --git a/src/components/Panels/SaleInfoPanel/DetailCard/index.module.scss b/src/components/Panels/SaleInfoPanel/DetailCard/index.module.scss index 3474c3f1..62b345e9 100644 --- a/src/components/Panels/SaleInfoPanel/DetailCard/index.module.scss +++ b/src/components/Panels/SaleInfoPanel/DetailCard/index.module.scss @@ -1,7 +1,7 @@ .container { display: flex; flex-direction: column; - padding: 1rem 2rem; + padding: 1rem 1.5rem; border-radius: 0.5rem; gap: 1.5rem; } diff --git a/src/components/Panels/SalePhaseInfoPanel/index.tsx b/src/components/Panels/SalePhaseInfoPanel/index.tsx index 97b5b4f8..b2c2199f 100644 --- a/src/components/Panels/SalePhaseInfoPanel/index.tsx +++ b/src/components/Panels/SalePhaseInfoPanel/index.tsx @@ -1,34 +1,25 @@ import { Box, Button, Paper, Typography, useTheme } from '@mui/material'; -import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { CountDown, SalePhaseCard } from '@/components/Elements'; +import { PurchaseHistoryModal } from '@/components/Modals'; -import { useNetwork } from '@/contexts/network'; import { useSaleInfo } from '@/contexts/sales'; import { SalePhase } from '@/models'; import styles from './index.module.scss'; export const SalePhaseInfoPanel = () => { const theme = useTheme(); - const router = useRouter(); - const { network } = useNetwork(); const { phase: { currentPhase, endpoints }, } = useSaleInfo(); const [remainingTime, setRemainingTime] = useState(0); + const [historyModalOpen, openHistoryModal] = useState(false); const valEndpoints = JSON.stringify(endpoints); - const onManage = () => { - router.push({ - pathname: '/regions', - query: { network }, - }); - }; - useEffect(() => { let _remainingTime; @@ -46,38 +37,44 @@ export const SalePhaseInfoPanel = () => { }, [valEndpoints, currentPhase]); return ( - - - - Current Phase - + <> + + + + Current Phase + - - - - - + + + + + + + Ends in: + - Ends in: - - - + + openHistoryModal(false)} + /> + ); }; diff --git a/src/components/Paras/index.ts b/src/components/Paras/index.ts index 5dac0e6f..cbde26a3 100644 --- a/src/components/Paras/index.ts +++ b/src/components/Paras/index.ts @@ -1,5 +1,4 @@ export * from './CoreExpiryCard'; export * from './LeaseStateCard'; -export * from './ParachainTable'; export * from './ParaDisplay'; export * from './ParaStateCard'; diff --git a/src/components/Paras/ParachainTable/index.module.scss b/src/components/Tables/ParachainTable/index.module.scss similarity index 100% rename from src/components/Paras/ParachainTable/index.module.scss rename to src/components/Tables/ParachainTable/index.module.scss diff --git a/src/components/Paras/ParachainTable/index.tsx b/src/components/Tables/ParachainTable/index.tsx similarity index 88% rename from src/components/Paras/ParachainTable/index.tsx rename to src/components/Tables/ParachainTable/index.tsx index b03380cc..cbbfeaef 100644 --- a/src/components/Paras/ParachainTable/index.tsx +++ b/src/components/Tables/ParachainTable/index.tsx @@ -10,8 +10,6 @@ import { styled, Table, TableBody, - TableCell, - tableCellClasses, TableContainer, TableFooter, TableHead, @@ -21,16 +19,19 @@ import { useTheme, } from '@mui/material'; import Image from 'next/image'; -import Link from 'next/link'; import React, { useEffect, useState } from 'react'; +import { Link } from '@/components/Elements'; + +import { SUBSCAN_URL } from '@/consts'; import { useRelayApi } from '@/contexts/apis'; import { useNetwork } from '@/contexts/network'; import { ParachainInfo, ParaState } from '@/models'; -import { CoreExpiryCard } from '../CoreExpiryCard'; -import { LeaseStateCard } from '../LeaseStateCard'; -import { ParaStateCard } from '../ParaStateCard'; +import { StyledTableCell, StyledTableRow } from '../common'; +import { CoreExpiryCard } from '../../Paras/CoreExpiryCard'; +import { LeaseStateCard } from '../../Paras/LeaseStateCard'; +import { ParaStateCard } from '../../Paras/ParaStateCard'; export type Order = 'asc' | 'desc'; @@ -48,28 +49,6 @@ interface ParachainTableProps { handleSort: (_orderBy: string, _direction: Order) => void; } -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white, - fontSize: '1rem', - }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14, - color: theme.palette.common.black, - }, -})); - -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover, - }, - // hide last border - '&:last-child td, &:last-child th': { - border: 0, - }, -})); - const ParaActionButton = styled(Button)(({ theme }: any) => ({ width: 'mind-width', fontWeight: 'bold', @@ -179,15 +158,9 @@ export const ParachainTable = ({ ).map(({ id, name, state, watching, logo, homepage }, index) => ( - + diff --git a/src/components/Tables/PurchaseHistoryTable/index.tsx b/src/components/Tables/PurchaseHistoryTable/index.tsx new file mode 100644 index 00000000..7fc47b8b --- /dev/null +++ b/src/components/Tables/PurchaseHistoryTable/index.tsx @@ -0,0 +1,97 @@ +import { + Paper, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; +import TimeAgo from 'javascript-time-ago'; +import en from 'javascript-time-ago/locale/en'; + +import { planckBnToUnit } from '@/utils/functions'; + +import { Address, Link } from '@/components/Elements'; + +import { SUBSCAN_URL } from '@/consts'; +import { useCoretimeApi } from '@/contexts/apis'; +import { useNetwork } from '@/contexts/network'; +import { PurchaseHistoryItem } from '@/models'; + +import { StyledTableCell } from '../common'; + +interface PurchaseHistoryTableProps { + data: PurchaseHistoryItem[]; +} + +export const PurchaseHistoryTable = ({ data }: PurchaseHistoryTableProps) => { + TimeAgo.addLocale(en); + // Create formatter (English). + const timeAgo = new TimeAgo('en-US'); + + const { network } = useNetwork(); + const { + state: { symbol, decimals }, + } = useCoretimeApi(); + + return ( + + + + + Extrinsic Idx + Account + Core + {`Price (${symbol})`} + Sales Type + Timestamp + + + + {data.map( + ( + { address, core, extrinsic_index, timestamp, price, type }, + index + ) => ( + + + + {extrinsic_index} + + + {/** FIXME: replace with the Address component */} + + +
+ + + {core} + + {planckBnToUnit(price.toString(), decimals).toFixed(2)} + + {type} + + {timeAgo.format(timestamp * 1000, 'round-minute')} + + + ) + )} + +
+
+ ); +}; diff --git a/src/components/Tables/common.ts b/src/components/Tables/common.ts new file mode 100644 index 00000000..94c0fa6b --- /dev/null +++ b/src/components/Tables/common.ts @@ -0,0 +1,24 @@ +import { styled, TableCell, tableCellClasses, TableRow } from '@mui/material'; + +export const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + fontSize: '1rem', + textAlign: 'center', + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + color: theme.palette.common.black, + }, +})); + +export const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0, + }, +})); diff --git a/src/components/Tables/index.ts b/src/components/Tables/index.ts new file mode 100644 index 00000000..f240661d --- /dev/null +++ b/src/components/Tables/index.ts @@ -0,0 +1,3 @@ +export * from './common'; +export * from './ParachainTable'; +export * from './PurchaseHistoryTable'; diff --git a/src/components/index.ts b/src/components/index.ts index f18ad175..abf09a1c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,3 +7,4 @@ export * from './Panels'; export * from './Paras'; export * from './Regions'; export * from './Sidebar'; +export * from './Tables'; diff --git a/src/consts/index.ts b/src/consts/index.ts new file mode 100644 index 00000000..415b4994 --- /dev/null +++ b/src/consts/index.ts @@ -0,0 +1,11 @@ +import { NetworkType } from '@/models'; + +export const CORETIME_API = { + [NetworkType.ROCOCO]: process.env.ROCOCO_CORETIME_API ?? '', + [NetworkType.KUSAMA]: process.env.KUSAMA_CORETIME_API ?? '', +}; + +export const SUBSCAN_URL = { + [NetworkType.ROCOCO]: 'https://rococo.subscan.io', + [NetworkType.KUSAMA]: 'https://kusama.subscan.io', +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 5194cd57..fdf73a82 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from './parasInfo'; +export * from './purchaseHistory'; export * from './renewableParas'; diff --git a/src/hooks/purchaseHistory.ts b/src/hooks/purchaseHistory.ts new file mode 100644 index 00000000..39fd41fd --- /dev/null +++ b/src/hooks/purchaseHistory.ts @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; + +import { CORETIME_API } from '@/consts'; +import { + NetworkType, + PurchaseHistoryItem, + PurchaseHistoryResponse, +} from '@/models'; + +export const usePurchaseHistory = ( + network: NetworkType, + regionBegin: number, + page: number, + row: number +) => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [isError, setError] = useState(false); + + useEffect(() => { + const asyncFetchData = async () => { + setLoading(true); + try { + const res = await fetch(`${CORETIME_API[network]}/broker/purchased`, { + method: 'POST', + body: JSON.stringify({ + region_begin: regionBegin, + row, + page, + }), + }); + if (res.status !== 200) { + setError(true); + } else { + const jsonData = await res.json(); + if (jsonData.message !== 'Success') { + setError(true); + setData([]); + } else { + if (jsonData.data.count == 0) { + setData([]); + setLoading(false); + return; + } + const data = jsonData.data as PurchaseHistoryResponse; + setData( + data.list.map( + ({ + account: { address }, + core, + extrinsic_index, + block_timestamp, + price, + purchased_type, + }) => + ({ + address, + core, + extrinsic_index, + timestamp: block_timestamp, + price, + type: purchased_type, + }) as PurchaseHistoryItem + ) + ); + } + } + } catch { + setError(true); + } + setLoading(false); + }; + asyncFetchData(); + }, [network, regionBegin, page, row]); + + return { + loading, + data, + isError, + }; +}; diff --git a/src/models/types.ts b/src/models/types.ts index 66d7472c..8d8f9dfb 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -300,3 +300,34 @@ export type BrokerStatus = { lastCommittedTimeslice: number; lastTimeslice: number; }; + +export type PurchaseHistoryResponseItem = { + account: { + address: string; + }; + begin: number; + block_num: number; + block_timestamp: number; + core: number; + core_index: string; + end: number; + event_index: string; + extrinsic_index: string; + mask: string; + price: number; + purchased_type: string; +}; + +export type PurchaseHistoryResponse = { + count: number; + list: PurchaseHistoryResponseItem[]; +}; + +export type PurchaseHistoryItem = { + address: string; + core: number; + extrinsic_index: string; + timestamp: number; + price: number; + type: string; +}; diff --git a/src/pages/purchase.tsx b/src/pages/purchase.tsx index 67207fe4..f82cebf9 100644 --- a/src/pages/purchase.tsx +++ b/src/pages/purchase.tsx @@ -1,12 +1,14 @@ import { Backdrop, Box, + Button, CircularProgress, Typography, useTheme, } from '@mui/material'; import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en.json'; +import { useRouter } from 'next/router'; import { useState } from 'react'; import { sendTx } from '@/utils/functions'; @@ -23,6 +25,7 @@ import { useAccounts } from '@/contexts/account'; import { useCoretimeApi } from '@/contexts/apis'; import { ApiState } from '@/contexts/apis/types'; import { useBalances } from '@/contexts/balance'; +import { useNetwork } from '@/contexts/network'; import { useRegions } from '@/contexts/regions'; import { useSaleInfo } from '@/contexts/sales'; import { useToast } from '@/contexts/toast'; @@ -48,6 +51,8 @@ const Purchase = () => { const { state: { api, apiState }, } = useCoretimeApi(); + const router = useRouter(); + const { network } = useNetwork(); const { fetchRegions } = useRegions(); @@ -81,6 +86,13 @@ const Purchase = () => { }); }; + const onManage = () => { + router.push({ + pathname: '/regions', + query: { network }, + }); + }; + return ( { +