diff --git a/src/components/app-body/counter-offers/cancel-counter-offer-btn.js b/src/components/app-body/counter-offers/cancel-counter-offer-btn.js
new file mode 100644
index 0000000..4724a28
--- /dev/null
+++ b/src/components/app-body/counter-offers/cancel-counter-offer-btn.js
@@ -0,0 +1,42 @@
+/*
+ This component renders as a button. When clicked, it opens a modal that
+ cancels the counter offer.
+
+ This is a functional component with as little state as possible.
+*/
+
+// Global npm libraries
+import React, { useState } from 'react'
+import { Button, Modal, Container } from 'react-bootstrap'
+
+function CancelCounterOfferBtn (props) {
+ const [show, setShow] = useState(false)
+
+ const handleClose = () => {
+ setShow(false)
+ // props.instance.setState({ showModal: false })
+ }
+
+ const handleOpen = () => {
+ setShow(true)
+ }
+
+ return (
+ <>
+
+
+
+ Cancel
+
+
+
+ {/** ... */}
+
+
+
+
+ >
+ )
+}
+
+export default CancelCounterOfferBtn
diff --git a/src/components/app-body/counter-offers/counter-offer-card.js b/src/components/app-body/counter-offers/counter-offer-card.js
index d6641e5..d72f573 100644
--- a/src/components/app-body/counter-offers/counter-offer-card.js
+++ b/src/components/app-body/counter-offers/counter-offer-card.js
@@ -1,64 +1,79 @@
/*
- This Card component displays a counter offer with token icon, name, and price.
+ This Card component summarizes an SLP token.
+ if a token icon does not exist or cant be loaded , then display a default icon from Jdenticon library.
*/
// Global npm libraries
-import React, { useState } from 'react'
+import React, { useState, useEffect } from 'react'
import { Container, Row, Col, Card, Button } from 'react-bootstrap'
import Jdenticon from '@chris.troutner/react-jdenticon'
+// Local libraries
+import InfoButton from './info-button'
+import CancelCounterOfferBtn from './cancel-counter-offer-btn'
function CounterOfferCard (props) {
- const { offer } = props
- const [icon, setIcon] = useState(offer.tokenIcon)
+ const { token } = props
+ const [icon, setIcon] = useState(token.icon)
+
+ // Update icon state every token.icon changes
+ useEffect(() => {
+ setIcon(token.icon)
+ }, [token.icon])
return (
<>
-
-
- {/** If the icon is loaded, display it */}
- {icon && (
- {
- setIcon(null) // Set the icon to null if it fails to load the image url.
- }}
- />
- )}
+
+
+ {/** If the icon is loaded, display it */
+ icon && (
+ {
+ setIcon(null) // Set the icon to null if it fails to load the image url.
+ }}
+ />
+ )
+ }
- {/** If the icon is not loaded, display the Jdenticon */}
- {!icon && (
-
-
-
- )}
-
-
-
{offer.ticker}
+ {/** If the icon is not loaded, display the Jdenticon */
+ !icon && (
+
+ )
+ }
+
+
+ {/** Show spinner info if tokens are loaded but data is not loaded */
+ isLoading && (
+
+ Loading Counter Offers
+
+
+ )
+ }
+ {/** Show spinner info if tokens are loaded but data is not loaded */
+ !isLoading && !dataAreLoaded && counterOffers.length > 0 && (
+
+ Loading Token Data
+
+
+ )
+ }
+ {/** Show spinner info if tokens are loaded but icons are not loaded */
+ !isLoading && dataAreLoaded && !iconsAreLoaded && (
+
+ Loading Token Icons
+
+
+ )
+ }
+
+
)}
-
+
+
+
+
+ {generateCards()}
+
+ {/** Display a message if no tokens are found */}
+ {!isLoading && counterOffers.length === 0 && (
+
+ No tokens found in wallet
+
+ )}
+
+
+ >
)
}
diff --git a/src/components/app-body/counter-offers/info-button.js b/src/components/app-body/counter-offers/info-button.js
new file mode 100644
index 0000000..b3e7235
--- /dev/null
+++ b/src/components/app-body/counter-offers/info-button.js
@@ -0,0 +1,146 @@
+/*
+ This component renders as a button. When clicked, it opens a modal that
+ displays information about the token.
+
+ This is a functional component with as little state as possible.
+*/
+
+// Global npm libraries
+import React, { useState, useEffect } from 'react'
+import { Button, Modal, Container, Row, Col, Spinner } from 'react-bootstrap'
+
+// Takes a string as input. If it matches a pattern for a link, a JSX object is
+// returned with a link. Otherwise the original string is returned.
+function linkIfUrl (url) {
+ // Convert the URL into a link if it contains 'http'
+ if (url?.includes('http')) {
+ url = ({url})
+
+ //
+ } else if (url?.includes('ipfs://')) {
+ // Convert to a Filecoin link if its an IPFS reference.
+
+ const cid = url.substring(7)
+ url = ({url})
+ }
+
+ return url
+}
+
+function InfoButton (props) {
+ const [show, setShow] = useState(false)
+ const [mutableDataCid, setMutableDataCid] = useState(null)
+
+ const handleClose = () => {
+ setShow(false)
+ // props.instance.setState({ showModal: false })
+ }
+
+ const handleOpen = () => {
+ setShow(true)
+ }
+
+ // Convert the url property of the token to a link, if it matches common patterns.
+ let url = props.token.url
+ url = linkIfUrl(props.token.url)
+
+ // console.log('props.token: ', props.token)
+
+ // Get Cid from url
+ const parseCid = (url) => {
+ // get the cid from the url format 'ipfs://bafybeicem27xbzs65uvbcgykcmscsgln3lmhbfrcoec3gdttkdgtxv5acq
+ if (url && url.includes('ipfs://')) {
+ const cid = url.split('ipfs://')[1]
+ return cid
+ }
+ return url
+ }
+ // Get token user data if it exists and verify if it contains media or markdown
+ useEffect(() => {
+ try {
+ console.log('props token', props.token)
+ const userDataStr = props.token.userData
+ if (userDataStr) {
+ const userData = JSON.parse(userDataStr)
+
+ // If user data contains media or markdown, set the mutable data cid
+ if (userData?.media || userData?.markdown) {
+ setMutableDataCid(parseCid(props.token.tokenData.mutableData))
+ }
+ }
+ } catch (error) {
+ // Do nothing
+ }
+ }, [props.token, show])
+
+ return (
+ <>
+
+
+
+ Token Information
+
+
+
+
+