diff --git a/src/components/OfferList.tsx b/src/components/OfferList.tsx new file mode 100644 index 0000000..1ebe9e0 --- /dev/null +++ b/src/components/OfferList.tsx @@ -0,0 +1,153 @@ +import React from 'react'; + +import dayjs from 'dayjs'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; + +import { shortenAddress } from '@usedapp/core' + +import { Theme } from '@mui/material/styles'; +import makeStyles from '@mui/styles/makeStyles'; +import Typography from '@mui/material/Typography'; +import createStyles from '@mui/styles/createStyles'; + +import OfferIcon from '@mui/icons-material/ConnectWithoutContact'; + +import { utils } from "ethers"; + +import { PropsFromRedux } from '../containers/EventHistoryContainer'; + +import LinkWrapper from './LinkWrapper'; + +import { + IOfferRecord, + NetworkName, +} from '../interfaces'; + +import { + PROPY_LIGHT_GREY, +} from '../utils/constants'; + +import { + getEtherscanLinkByNetworkName, + priceFormat, +} from '../utils'; + +dayjs.extend(advancedFormat); + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + position: 'relative', + }, + content: { + width: '100%', + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + overflowX: 'scroll', + }, + offerRecord: { + width: '100%', + display: 'flex', + justifyContent: 'flex-start', + marginBottom: theme.spacing(2), + }, + eventIconOuterContainer: { + marginRight: theme.spacing(2), + }, + eventIconInnerContainer: { + backgroundColor: PROPY_LIGHT_GREY, + width: 40, + height: 40, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '50%', + color: '#38a5fc', + }, + eventRecordSummaryContainer: { + whiteSpace: 'nowrap', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }, + eventRecordSummaryLineOne: { + marginBottom: theme.spacing(0.5), + }, + }), +); + +interface ITokenOfferList { + offerRecords: IOfferRecord[] | null +} + +const getOfferIcon = () => { + return ; +} + +const renderAddress = (value: string) => { + let innerElement = {shortenAddress(value)}; + let link = getEtherscanLinkByNetworkName(NetworkName["ethereum"], value, 'address'); + if(link) { + return ( + + {innerElement} + + ) + } + return innerElement +} + +const getEventSummaryLineEntryOne = (event: IOfferRecord) => { + // mint + return ( +
+ {renderAddress(event.user_address)} +  offered {priceFormat(Number(utils.formatUnits(event.offer_token_amount, event.offer_token.decimals)), 2, event.offer_token.symbol, false)}  +
+ ); +} + +const getEventSummaryLineEntryTwo = (event: IOfferRecord) => { + return ( +
+ {event.timestamp_unix ? dayjs.unix(Number(event.timestamp_unix)).format('MMM-D-YYYY hh:mm A') : 'loading...'} +
+ ); +} + +const OfferList = (props: PropsFromRedux & ITokenOfferList) => { + const classes = useStyles(); + + const { + offerRecords, + } = props; + + return ( + <> +
+
+ {offerRecords && offerRecords.sort((a, b) => { + return Number(b.timestamp_unix) - Number(a.timestamp_unix) + }).map((offerRecord, index) => +
+
+
+ {getOfferIcon()} +
+
+
+
+ {getEventSummaryLineEntryOne(offerRecord)} +
+ {getEventSummaryLineEntryTwo(offerRecord)} +
+
+ )} +
+
+ + ) +}; + +export default OfferList; \ No newline at end of file diff --git a/src/components/SignalInterest.tsx b/src/components/SignalInterest.tsx index 7e8e95f..09e73e5 100644 --- a/src/components/SignalInterest.tsx +++ b/src/components/SignalInterest.tsx @@ -77,6 +77,7 @@ interface ISignalInterest { tokenAddress: string, tokenId: string, tokenNetwork: string, + onSuccess?: () => void, } const SignalInterest = (props: PropsFromRedux & ISignalInterest) => { @@ -95,6 +96,7 @@ const SignalInterest = (props: PropsFromRedux & ISignalInterest) => { tokenAddress, tokenId, tokenNetwork, + onSuccess, } = props; const signOfferMessage = async (proAmount: string) => { @@ -129,6 +131,9 @@ const SignalInterest = (props: PropsFromRedux & ISignalInterest) => { let triggerSignedMessageActionResponse = await SignerService.validateSignedMessageAndPerformAction(messageForSigning, signedMessage, signerAccount); console.log({triggerSignedMessageActionResponse}); if(triggerSignedMessageActionResponse.status) { + if(onSuccess) { + onSuccess(); + } toast.success("Offer placed successfully!"); setShowDialog(false); } diff --git a/src/containers/OfferListContainer.tsx b/src/containers/OfferListContainer.tsx new file mode 100644 index 0000000..0434c47 --- /dev/null +++ b/src/containers/OfferListContainer.tsx @@ -0,0 +1,19 @@ +import { connect, ConnectedProps } from 'react-redux'; + +import OfferList from '../components/OfferList'; + +interface RootState { + isConsideredMobile: boolean + isConsideredMedium: boolean +} + +const mapStateToProps = (state: RootState) => ({ + isConsideredMobile: state.isConsideredMobile, + isConsideredMedium: state.isConsideredMedium, +}) + +const connector = connect(mapStateToProps, {}) + +export type PropsFromRedux = ConnectedProps + +export default connector(OfferList) \ No newline at end of file diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 07fe27f..4573541 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -185,4 +185,14 @@ export interface INonceResponse { nonce: number; salt: string; } +} + +export interface IOfferRecord { + asset_address: string; + offer_token_address: string; + offer_token_amount: string; + timestamp_unix: number; + token_id: string; + user_address: string; + offer_token: IAssetRecord; } \ No newline at end of file diff --git a/src/pages/SingleTokenPage.tsx b/src/pages/SingleTokenPage.tsx index 115adcb..dec4381 100644 --- a/src/pages/SingleTokenPage.tsx +++ b/src/pages/SingleTokenPage.tsx @@ -19,6 +19,7 @@ import GenericTitleContainer from '../containers/GenericTitleContainer'; import EventHistoryContainer from '../containers/EventHistoryContainer'; import TokenMetadataTimelineContainer from '../containers/TokenMetadataTimelineContainer'; import SignalInterestContainer from '../containers/SignalInterestContainer'; +import OfferListContainer from '../containers/OfferListContainer'; import LinkWrapper from '../components/LinkWrapper'; @@ -36,6 +37,7 @@ import { ITokenMetadata, ITransferEventERC20Record, ITransferEventERC721Record, + IOfferRecord, TokenStandard, } from '../interfaces'; @@ -111,6 +113,7 @@ const SingleTokenPage = () => { const [tokenRecord, setTokenRecord] = useState(null); const [tokenEventRecord, setTokenEventRecord] = useState(null); + const [tokenOfferList, setTokenOfferList] = useState(null); const [tokenMetadata, setTokenMetadata] = useState(null); const [fetchIndex, setFetchIndex] = useState(0); const [isMetadataRefreshing, setIsMetadataRefreshing] = useState(false); @@ -146,6 +149,9 @@ const SingleTokenPage = () => { if(tokenRecordQueryResponse?.data?.transfer_events_erc20) { setTokenEventRecord(tokenRecordQueryResponse?.data?.transfer_events_erc20); } + if(tokenRecordQueryResponse?.data?.offchain_offers) { + setTokenOfferList(tokenRecordQueryResponse?.data?.offchain_offers); + } if(tokenRecordQueryResponse?.data?.metadata) { let metadata = JSON.parse(tokenRecordQueryResponse?.data?.metadata); // // temp timeline shim for testing design @@ -261,8 +267,11 @@ const SingleTokenPage = () => { {tokenEventRecord && } {allowSignalInterest && tokenId && tokenAddress && network && <> - - + + setFetchIndex(fetchIndex + 1)} tokenId={tokenId} tokenAddress={tokenAddress} tokenNetwork={network} /> +
+ {tokenEventRecord && } +
}