diff --git a/index.html b/index.html index 0ec4677..a31d64f 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,7 @@ - + @@ -14,7 +12,7 @@ - Qort.Trade + Q-Trade
diff --git a/package-lock.json b/package-lock.json index 9a44d49..95abc3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,12 @@ "lodash": "^4.17.21", "moment": "^2.30.1", "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", + "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", + "react-loader-spinner": "^6.1.6", + "react-qr-code": "^2.0.15", "react-router-dom": "^6.23.0", "react-toastify": "^10.0.5", "sass": "^1.76.0", @@ -3236,6 +3240,11 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -3826,6 +3835,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001660", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", @@ -3952,6 +3969,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", @@ -4003,6 +4028,24 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5984,7 +6027,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -6218,7 +6260,6 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6242,6 +6283,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6292,6 +6338,11 @@ "node": ">=6" } }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6332,6 +6383,26 @@ "node": ">=0.10.0" } }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, + "node_modules/react-countdown-circle-timer": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/react-countdown-circle-timer/-/react-countdown-circle-timer-3.2.1.tgz", + "integrity": "sha512-yBAy/9ILXOiFbLBM+3jS72TW5LeRcH8wkRC9NNqMpUkCXkGjSnaeRbJMsR9lsYF0oVXjSDbJaRbCuVMT+9HnKA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6354,6 +6425,34 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-loader-spinner": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz", + "integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^6.1.2" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-qr-code": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", + "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -6788,6 +6887,11 @@ "node": ">= 0.4" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7041,6 +7145,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -7189,6 +7325,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -7210,6 +7351,11 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 25ab599..77087a4 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,12 @@ "lodash": "^4.17.21", "moment": "^2.30.1", "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", + "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", + "react-loader-spinner": "^6.1.6", + "react-qr-code": "^2.0.15", "react-router-dom": "^6.23.0", "react-toastify": "^10.0.5", "sass": "^1.76.0", diff --git a/src/App-styles.tsx b/src/App-styles.tsx index b50acf2..81cd6f6 100644 --- a/src/App-styles.tsx +++ b/src/App-styles.tsx @@ -2,13 +2,15 @@ import { Box } from "@mui/material"; import { styled } from "@mui/system"; export const AppContainer = styled(Box)(({ theme }) => ({ - width: "100%", height: "100%", display: "flex", flexDirection: "column", alignItems: "flex-start", - padding: "1em 0", - paddingBottom: '50px' + padding: "20px 30px 0 30px", + backgroundColor: "#323336", + [`@media (max-width: 500px)`]: { + padding: "10px 5px 0 5px", + } })); export const MainContainer = styled(Box)` diff --git a/src/App.css b/src/App.css index 2c5cd42..6639473 100644 --- a/src/App.css +++ b/src/App.css @@ -124,4 +124,23 @@ src: url('./assets/fonts/Inter-SemiBold.ttf') format('truetype'); font-weight: 600; font-display: swap; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} +::-webkit-scrollbar-track:hover { + background-color: transparent; +} + +::-webkit-scrollbar { + width: 14px; + height: 10px; +} + +::-webkit-scrollbar-thumb { + background-color: #444444; + border-radius: 8px; + background-clip: content-box; + border: 4px solid transparent; } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 46c2081..1acdd92 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import ReactGA from "react-ga4"; import "./App.css"; import socketService from "./services/socketService"; @@ -71,7 +71,13 @@ export async function sendRequestToExtension( function App() { const [userInfo, setUserInfo] = useState(null); const [qortBalance, setQortBalance] = useState(null); - const [ltcBalance, setLtcBalance] = useState(null); + const [balances, setBalances] = useState({}); + const [selectedCoin, setSelectedCoin] = useState("LITECOIN"); + + const foreignCoinBalance = useMemo(()=> { + if(balances[selectedCoin] === 0) return 0 + return balances[selectedCoin] || null + }, [balances, selectedCoin]) const [isAuthenticated, setIsAuthenticated] = useState(false); const [OAuthLoading, setOAuthLoading] = useState(false); const db = useIndexedDBContext(); @@ -172,14 +178,19 @@ function App() { setQortBalance(balanceResponse.data?.value) } - const getLTCBalance = async () => { + const getLTCBalance = async (coin) => { try { const response = await qortalRequest({ action: "GET_WALLET_BALANCE", - coin: "LTC" + coin: getCoinLabel(coin) }); if(!response?.error){ - setLtcBalance(+response) + setBalances((prev)=> { + return { + ...prev, + [coin]: +response + } + }) } } catch (error) { // @@ -187,18 +198,18 @@ function App() { } useEffect(() => { - if(!userInfo?.address) return + if(!userInfo?.address || !selectedCoin) return const intervalGetTradeInfo = setInterval(() => { fetchOngoingTransactions() - getLTCBalance() + getLTCBalance(selectedCoin) getQortBalance() }, 150000) - getLTCBalance() + getLTCBalance(selectedCoin) getQortBalance() return () => { clearInterval(intervalGetTradeInfo) } - }, [userInfo?.address, isAuthenticated]) + }, [userInfo?.address, isAuthenticated, selectedCoin]) const handleMessage = async (event: any) => { @@ -208,7 +219,6 @@ function App() { setAvatar(""); setIsAuthenticated(false); setQortBalance(null) - setLtcBalance(null) localStorage.setItem("token", ""); } else if(event.data.type === "RESPONSE_FOR_TRADES"){ @@ -246,6 +256,37 @@ function App() { }; }, [userInfo?.address]); + const getCoinLabel = useCallback((coin?: string)=> { + switch(coin || selectedCoin){ + case "LITECOIN":{ + + return 'LTC' + } + case "DOGECOIN":{ + + return 'DOGE' + } + case "BITCOIN":{ + + return 'BTC' + } + case "DIGIBYTE":{ + + return 'DGB' + } + case "RAVENCOIN":{ + + return 'RVN' + } + case "PIRATECHAIN":{ + + return 'ARRR' + } + default: + return null + } + }, [selectedCoin]) + const gameContextValue: IContextProps = { userInfo, setUserInfo, @@ -253,7 +294,7 @@ function App() { setUserNameAvatar, onGoingTrades, fetchOngoingTransactions, - ltcBalance, + foreignCoinBalance, qortBalance, isAuthenticated, setIsAuthenticated, @@ -261,7 +302,7 @@ function App() { setOAuthLoading, updateTransactionInDB, sellOrders, - deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, isUsingGateway + deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, isUsingGateway, selectedCoin, setSelectedCoin, getCoinLabel }; diff --git a/src/assets/SVG/Copy.svg b/src/assets/SVG/Copy.svg new file mode 100644 index 0000000..0348fc9 --- /dev/null +++ b/src/assets/SVG/Copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/arrr.png b/src/assets/img/arrr.png new file mode 100644 index 0000000..d274565 Binary files /dev/null and b/src/assets/img/arrr.png differ diff --git a/src/assets/img/btc.png b/src/assets/img/btc.png new file mode 100644 index 0000000..fe3fd1a Binary files /dev/null and b/src/assets/img/btc.png differ diff --git a/src/assets/img/dgb.png b/src/assets/img/dgb.png new file mode 100644 index 0000000..6950158 Binary files /dev/null and b/src/assets/img/dgb.png differ diff --git a/src/assets/img/doge.png b/src/assets/img/doge.png new file mode 100644 index 0000000..d99fa2f Binary files /dev/null and b/src/assets/img/doge.png differ diff --git a/src/assets/img/ltc.png b/src/assets/img/ltc.png new file mode 100644 index 0000000..d743e57 Binary files /dev/null and b/src/assets/img/ltc.png differ diff --git a/src/assets/img/qort.png b/src/assets/img/qort.png new file mode 100644 index 0000000..39d090f Binary files /dev/null and b/src/assets/img/qort.png differ diff --git a/src/assets/img/rvn.png b/src/assets/img/rvn.png new file mode 100644 index 0000000..31d7fdd Binary files /dev/null and b/src/assets/img/rvn.png differ diff --git a/src/components/DbComponents/OngoingTransactions.tsx b/src/components/DbComponents/OngoingTransactions.tsx index 566393a..4430e64 100644 --- a/src/components/DbComponents/OngoingTransactions.tsx +++ b/src/components/DbComponents/OngoingTransactions.tsx @@ -5,7 +5,7 @@ import { useIndexedDBContext } from "../../contexts/indexedDBContext"; const fetchTradeInfo = async (qortalAtAddress) => { - const checkIfOfferingRes = await fetch(`http://127.0.0.1:12391/crosschain/trade/${qortalAtAddress}`) + const checkIfOfferingRes = await fetch(`/crosschain/trade/${qortalAtAddress}`) const data = await checkIfOfferingRes.json() return data }; diff --git a/src/components/Grids/OngoingTrades.tsx b/src/components/Grids/OngoingTrades.tsx index 5a62d42..65eb4d4 100644 --- a/src/components/Grids/OngoingTrades.tsx +++ b/src/components/Grids/OngoingTrades.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { AgGridReact } from 'ag-grid-react'; -import { ColDef, SizeColumnsToContentStrategy } from 'ag-grid-community'; +import { ColDef, RowClassParams, RowStyle, SizeColumnsToContentStrategy } from 'ag-grid-community'; import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-alpine.css'; import gameContext from '../../contexts/gameContext'; @@ -10,9 +10,19 @@ const autoSizeStrategy: SizeColumnsToContentStrategy = { }; export const OngoingTrades = () => { - const { onGoingTrades } = useContext(gameContext); - + const { onGoingTrades, getCoinLabel, selectedCoin } = useContext(gameContext); + const gridRef = useRef(null) + const filteredOngoingTrades = useMemo(()=> { + return onGoingTrades?.filter((item)=> item?.tradeInfo?.foreignBlockchain === selectedCoin) + }, [onGoingTrades, selectedCoin]) + + const onGridReady = useCallback((params: any) => { + // params.api.sizeColumnsToFit(); // Adjust columns to fit the grid width + // const allColumnIds = params.columnApi.getAllColumns().map((col: any) => col.getColId()); + // params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content + }, []); + const defaultColDef = { resizable: true, // Make columns resizable by default sortable: true, // Make columns sortable by default @@ -35,9 +45,10 @@ export const OngoingTrades = () => { resizable: true , flex: 1, minWidth: 100 }, - { headerName: "Amount (QORT)", valueGetter: (params) => +params.data.tradeInfo.qortAmount, resizable: true, flex: 1, minWidth: 100 }, - { headerName: "LTC/QORT", valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount / +params.data.tradeInfo.qortAmount , resizable: true , flex: 1, minWidth: 100}, - { headerName: "Total LTC Value", valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount, resizable: true , flex: 1, minWidth: 100 }, + { headerName: "Amount (QORT)", valueGetter: (params) => +params.data.tradeInfo.qortAmount, resizable: true, flex: 1, minWidth: 150 }, + { headerName: `${getCoinLabel()}/QORT`, valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount / +params.data.tradeInfo.qortAmount , resizable: true , flex: 1, minWidth: 150}, + { headerName: `Total ${getCoinLabel()} Value`, valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount, resizable: true , flex: 1, minWidth: 150, + }, { headerName: "Notes", valueGetter: (params) => { if (params.data.tradeInfo.mode === 'TRADING') { @@ -54,7 +65,7 @@ export const OngoingTrades = () => { } if (params.data.message) return params.data.message - }, resizable: true, flex: 1, minWidth: 100 + }, resizable: true, flex: 1, minWidth:300, autoHeight: true, cellStyle: { whiteSpace: 'normal', wordBreak: 'break-word', padding: '5px' }, } ]; @@ -65,15 +76,20 @@ export const OngoingTrades = () => { // return null; // }; const getRowId = useCallback(function (params: any) { - return String(params.data._id); + return String(params.data?.qortalAtAddress); }, []); + + return ( -
+
{ // domLayout='autoHeight' // getRowStyle={getRowStyle} - + />
); diff --git a/src/components/Grids/Table-styles.tsx b/src/components/Grids/Table-styles.tsx index f6feb41..4ea70fa 100644 --- a/src/components/Grids/Table-styles.tsx +++ b/src/components/Grids/Table-styles.tsx @@ -1,11 +1,62 @@ -import { styled } from "@mui/system"; -import { Box, Typography } from "@mui/material"; +import { Box, styled } from "@mui/system"; +import { Button, Typography } from "@mui/material"; + +export const MainContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + width: "100%", + height: "100%", +}); export const TextTableTitle = styled(Typography)(({ theme }) => ({ - fontFamily: "Inter", - color: theme.palette.text.primary, - fontWeight: 400, - fontSize: "20px", - lineHeight: "40px", - userSelect: "none", - })); \ No newline at end of file + fontFamily: "Inter", + color: theme.palette.text.primary, + fontWeight: 400, + fontSize: "20px", + lineHeight: "40px", + userSelect: "none", +})); + +export const BuyContainer = styled(Box)(({ theme }) => ({ + position: "fixed", + width: "calc(100% - 14px)", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + bottom: "0px", + height: "100px", + padding: "18px 14px 12px 14px", + background: "#323336", + zIndex: 3, + [theme.breakpoints.down("sm")]: { + width: "calc(100% - 2px)", + } +})); + +export const BuyContainerDivider = styled(Box)(({ theme }) => ({ + position: "absolute", + width: "60%", + height: "1px", + background: "lightgray", + top: "10px", + left: "50%", + transform: "translateX(-50%)", + [theme.breakpoints.down("sm")]: { + top: "5px", + } +})); + +export const BuyOrderBtn = styled("button")(({ theme }) => ({ + borderRadius: "8px", + width: "74px", + height: "30px", + background: "#4D7345", + color: "white", + cursor: "pointer", + border: "1px solid #375232", + boxShadow: "0px 2.77px 2.21px 0px #00000005", + marginRight: "10px", + [theme.breakpoints.down("sm")]: { + marginRight: "0px", + } +})); diff --git a/src/components/Grids/TradeOffers.tsx b/src/components/Grids/TradeOffers.tsx index 03c4e52..aba4c44 100644 --- a/src/components/Grids/TradeOffers.tsx +++ b/src/components/Grids/TradeOffers.tsx @@ -1,15 +1,51 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { AgGridReact } from 'ag-grid-react'; -import { ColDef, RowClassParams, RowStyle, SizeColumnsToContentStrategy } from 'ag-grid-community'; -import 'ag-grid-community/styles/ag-grid.css'; -import 'ag-grid-community/styles/ag-theme-alpine.css'; -import axios from 'axios'; -import { sendRequestToExtension } from '../../App'; -import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Snackbar, SnackbarCloseReason, Typography } from '@mui/material'; -import gameContext from '../../contexts/gameContext'; -import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; -import { useModal } from '../common/useModal'; -import FileSaver from 'file-saver'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { AgGridReact } from "ag-grid-react"; +import { + ColDef, + RowClassParams, + RowStyle, + SizeColumnsToContentStrategy, +} from "ag-grid-community"; +import "ag-grid-community/styles/ag-grid.css"; +import "ag-grid-community/styles/ag-theme-alpine.css"; +import { + Alert, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Snackbar, + SnackbarCloseReason, + Typography, +} from "@mui/material"; +import gameContext from "../../contexts/gameContext"; +import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import { useModal } from "../common/useModal"; +import FileSaver from "file-saver"; +import { Spacer } from "../common/Spacer"; +import { Hourglass } from "react-loader-spinner"; +import ErrorIcon from "@mui/icons-material/Error"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import { CountdownCircleTimer } from "react-countdown-circle-timer"; +import { + BuyContainer, + BuyContainerDivider, + BuyOrderBtn, + MainContainer, +} from "./Table-styles"; + +export const baseLocalHost = window.location.host; +// export const baseLocalHost = "127.0.0.1:12391"; interface RowData { amountQORT: number; @@ -19,26 +55,35 @@ interface RowData { } export const saveFileToDisk = async (data) => { - const dataString = JSON.stringify(data); - const blob = new Blob([dataString], { type: 'application/json' }); -const fileName = "traderecord_" + Date.now() + '_' + ".json"; + const blob = new Blob([dataString], { type: "application/json" }); + const fileName = "traderecord_" + Date.now() + "_" + ".json"; -await FileSaver.saveAs(blob, fileName); - -} - -export const autoSizeStrategy: SizeColumnsToContentStrategy = { - type: 'fitCellContents' + await FileSaver.saveAs(blob, fileName); }; -export const TradeOffers: React.FC = ({ltcBalance}:any) => { - const [offers, setOffers] = useState([]) - - const { fetchOngoingTransactions, onGoingTrades, updateTransactionInDB, isUsingGateway } = useContext(gameContext); - const listOfOngoingTradesAts = useMemo(()=> { - return onGoingTrades?.filter((item)=> item?.status !== 'trade-failed')?.map((trade)=> trade?.qortalAtAddress) || [] - }, [onGoingTrades]) +export const autoSizeStrategy: SizeColumnsToContentStrategy = { + type: "fitCellContents", +}; + +export const TradeOffers: React.FC = ({ foreignCoinBalance }: any) => { + const [offers, setOffers] = useState([]); + const [qortalNames, setQortalNames] = useState({}); + const { + fetchOngoingTransactions, + onGoingTrades, + updateTransactionInDB, + isUsingGateway, + getCoinLabel, + selectedCoin, + } = useContext(gameContext); + const listOfOngoingTradesAts = useMemo(() => { + return ( + onGoingTrades + ?.filter((item) => item?.status !== "trade-failed") + ?.map((trade) => trade?.qortalAtAddress) || [] + ); + }, [onGoingTrades]); const { isShow: isShowInfo, onCancel: onCancelInfo, @@ -47,63 +92,133 @@ export const TradeOffers: React.FC = ({ltcBalance}:any) => { message: messageInfo, } = useModal(); - const offersWithoutOngoing = useMemo(()=> { - return offers.filter((item)=> !listOfOngoingTradesAts.includes(item.qortalAtAddress)) - }, [listOfOngoingTradesAts, offers]) - - - - const [selectedOffer, setSelectedOffer] = useState(null) - const [selectedOffers, setSelectedOffers] = useState([]) - const [record, setRecord] = useState(null) - const tradePresenceTxns = useRef([]) - const offeringTrades = useRef([]) - const blockedTradesList = useRef([]) - const gridRef = useRef(null) - - - const [open, setOpen] = useState(false) - const [info, setInfo] = useState(null) - const BuyButton = () => { - return ( - + const offersWithoutOngoing = useMemo(() => { + return offers.filter( + (item) => !listOfOngoingTradesAts.includes(item.qortalAtAddress) ); + }, [listOfOngoingTradesAts, offers]); + const initiatedFetchPresence = useRef(false); + const initiatedFetchPresenceSocket = useRef(false); + const [isShowBuyInProgress, setIsShowBuyInProgress] = useState(null); + const socketRef = useRef(null); + const socketPresenceRef = useRef(null); + const [selectedOffer, setSelectedOffer] = useState(null); + const [selectedOffers, setSelectedOffers] = useState([]); + const [record, setRecord] = useState(null); + const tradePresenceTxns = useRef([]); + const offeringTrades = useRef([]); + const blockedTradesList = useRef([]); + const gridRef = useRef(null); + + const [open, setOpen] = useState(false); + const [info, setInfo] = useState(null); + const BuyButton = () => { + return BUY; }; - + const defaultColDef = { resizable: true, // Make columns resizable by default sortable: true, // Make columns sortable by default suppressMovable: true, // Prevent columns from being movable }; - const columnDefs: ColDef[] = [ - { - headerCheckboxSelection: true, // Adds a checkbox in the header for selecting all rows - checkboxSelection: true, // Adds checkboxes in each row for selection - headerName: "Select", // You can customize the header name - width: 50, // Adjust the width as needed - pinned: 'left', // Optional, to pin this column on the left - resizable: false, - }, - { headerName: "QORT AMOUNT", field: "qortAmount" , flex: 1, // Flex makes this column responsive - minWidth: 150, // Ensure it doesn't shrink too much - resizable: true }, - { headerName: "LTC/QORT", valueGetter: (params) => +params.data.foreignAmount / +params.data.qortAmount, sortable: true, sort: 'asc', flex: 1, // Flex makes this column responsive - minWidth: 150, // Ensure it doesn't shrink too much - resizable: true }, - { headerName: "Total LTC Value", field: "foreignAmount", flex: 1, // Flex makes this column responsive - minWidth: 150, // Ensure it doesn't shrink too much - resizable: true }, - { headerName: "Seller", field: "qortalCreator", flex: 1, // Flex makes this column responsive - minWidth: 300, // Ensure it doesn't shrink too much - resizable: true }, - ]; + const getName = async (address) => { + try { + const response = await fetch("/names/address/" + address); + const nameData = await response.json(); + if (nameData?.length > 0) { + setQortalNames((prev) => { + return { + ...prev, + [address]: nameData[0].name, + }; + }); + } else { + setQortalNames((prev) => { + return { + ...prev, + [address]: null, + }; + }); + } + } catch (error) { + // error + } + }; - + const restartTradeOffers = () => { + if (socketRef.current) { + socketRef.current.close(1000, "forced"); // Close with a custom reason + socketRef.current = null; + } + offeringTrades.current = []; + setOffers([]); + setSelectedOffer(null); + }; + + const restartPresence = () => { + if (socketPresenceRef.current) { + socketPresenceRef.current.close(1000, "forced"); // Close with a custom reason + socketPresenceRef.current = null; + } + }; + + const columnDefs: ColDef[] = useMemo(() => { + return [ + { + headerCheckboxSelection: true, // Adds a checkbox in the header for selecting all rows + checkboxSelection: true, // Adds checkboxes in each row for selection + headerName: "", // You can customize the header name + width: 50, // Adjust the width as needed + pinned: "left", // Optional, to pin this column on the left + resizable: false, + }, + { + headerName: "QORT AMOUNT", + field: "qortAmount", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: `${getCoinLabel()}/QORT`, + valueGetter: (params) => + +params.data.foreignAmount / +params.data.qortAmount, + sortable: true, + sort: "asc", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: `Total ${getCoinLabel()} Value`, + field: "foreignAmount", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: "Seller", + field: "qortalCreator", + flex: 1, // Flex makes this column responsive + minWidth: 300, // Ensure it doesn't shrink too much + resizable: true, + valueGetter: (params) => { + if (params?.data?.qortalCreator) { + if (qortalNames[params?.data?.qortalCreator]) { + return qortalNames[params?.data?.qortalCreator]; + } else if (qortalNames[params?.data?.qortalCreator] === undefined) { + getName(params?.data?.qortalCreator); + + return params?.data?.qortalCreator; + } else { + return params?.data?.qortalCreator; + } + } + }, + }, + ]; + }, [qortalNames, getCoinLabel]); // const onRowClicked = (event: any) => { // if(listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return @@ -112,197 +227,256 @@ export const TradeOffers: React.FC = ({ltcBalance}:any) => { // }; const restartTradePresenceWebSocket = () => { - setTimeout(() => initTradePresenceWebSocket(true), 50) - } - - + restartPresence(); + setTimeout(() => initTradePresenceWebSocket(true), 50); + }; const getNewBlockedTrades = async () => { const unconfirmedTransactionsList = async () => { + const unconfirmedTransactionslUrl = `/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true`; - const unconfirmedTransactionslUrl = `/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true` + var addBlockedTrades = JSON.parse( + localStorage.getItem("failedTrades") || "[]" + ); - var addBlockedTrades = JSON.parse(localStorage.getItem('failedTrades') || '[]') - - await fetch(unconfirmedTransactionslUrl).then(response => { - return response.json() - }).then(data => { - data.map((item: any) => { - const unconfirmedNessageTimeDiff = Date.now() - item.timestamp - const timeOneHour = 60 * 60 * 1000 - if (Number(unconfirmedNessageTimeDiff) > Number(timeOneHour)) { - const addBlocked = { - timestamp: item.timestamp, - recipient: item.recipient - } - addBlockedTrades.push(addBlocked) - } + await fetch(unconfirmedTransactionslUrl) + .then((response) => { + return response.json(); }) - localStorage.setItem("failedTrades", JSON.stringify(addBlockedTrades)) - blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]') - }) - } + .then((data) => { + data.map((item: any) => { + const unconfirmedNessageTimeDiff = Date.now() - item.timestamp; + const timeOneHour = 60 * 60 * 1000; + if (Number(unconfirmedNessageTimeDiff) > Number(timeOneHour)) { + const addBlocked = { + timestamp: item.timestamp, + recipient: item.recipient, + }; + addBlockedTrades.push(addBlocked); + } + }); + localStorage.setItem( + "failedTrades", + JSON.stringify(addBlockedTrades) + ); + blockedTradesList.current = JSON.parse( + localStorage.getItem("failedTrades") || "[]" + ); + }); + }; - await unconfirmedTransactionsList() + await unconfirmedTransactionsList(); const filterUnconfirmedTransactionsList = async () => { - let cleanBlockedTrades = blockedTradesList.current.reduce((newArray, cut: any) => { - if (cut && !newArray.some((obj: any) => obj.recipient === cut.recipient)) { - newArray.push(cut) - } - return newArray - }, [] as any[]) - localStorage.setItem("failedTrades", JSON.stringify(cleanBlockedTrades)) - blockedTradesList.current = JSON.parse(localStorage.getItem("failedTrades") || "[]") - } + let cleanBlockedTrades = blockedTradesList.current.reduce( + (newArray, cut: any) => { + if ( + cut && + !newArray.some((obj: any) => obj.recipient === cut.recipient) + ) { + newArray.push(cut); + } + return newArray; + }, + [] as any[] + ); + localStorage.setItem("failedTrades", JSON.stringify(cleanBlockedTrades)); + blockedTradesList.current = JSON.parse( + localStorage.getItem("failedTrades") || "[]" + ); + }; - await filterUnconfirmedTransactionsList() - processOffersWithPresence() - } + await filterUnconfirmedTransactionsList(); + processOffersWithPresence(); + }; - const executeGetNewBlockTrades = useCallback(()=> { - getNewBlockedTrades() - - }, []) + const executeGetNewBlockTrades = useCallback(() => { + getNewBlockedTrades(); + }, []); useEffect(() => { subscribeToEvent("execute-get-new-block-trades", executeGetNewBlockTrades); return () => { - unsubscribeFromEvent("execute-get-new-block-trades", executeGetNewBlockTrades); + unsubscribeFromEvent( + "execute-get-new-block-trades", + executeGetNewBlockTrades + ); }; }, []); const processOffersWithPresence = () => { - if (offeringTrades.current === null) return + if (offeringTrades.current === null) return; async function asyncForEach(array: any, callback: any) { for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array) + await callback(array[index], index, array); } } - const filterOffersUsingTradePresence = (offeringTrade: any) => { return offeringTrade.tradePresenceExpiry > Date.now(); - } + }; const startOfferPresenceMapping = async () => { if (tradePresenceTxns.current) { for (const tradePresence of tradePresenceTxns.current) { - const offerIndex = offeringTrades.current.findIndex(offeringTrade => offeringTrade.qortalCreatorTradeAddress === tradePresence.tradeAddress); + const offerIndex = offeringTrades.current.findIndex( + (offeringTrade) => + offeringTrade.qortalCreatorTradeAddress === + tradePresence.tradeAddress + ); if (offerIndex !== -1) { - offeringTrades.current[offerIndex].tradePresenceExpiry = tradePresence.timestamp; + offeringTrades.current[offerIndex].tradePresenceExpiry = + tradePresence.timestamp; } } } - let filteredOffers = offeringTrades.current?.filter((offeringTrade) => filterOffersUsingTradePresence(offeringTrade)) || [] - let tradesPresenceCleaned: any[] = filteredOffers - + let filteredOffers = + offeringTrades.current?.filter((offeringTrade) => + filterOffersUsingTradePresence(offeringTrade) + ) || []; + let tradesPresenceCleaned: any[] = filteredOffers; blockedTradesList.current.forEach((item: any) => { - const toDelete = item.recipient - tradesPresenceCleaned = tradesPresenceCleaned?.filter(el => { - return el.qortalCreatorTradeAddress !== toDelete - }) || [] - }) + const toDelete = item.recipient; + tradesPresenceCleaned = + tradesPresenceCleaned?.filter((el) => { + return el.qortalCreatorTradeAddress !== toDelete; + }) || []; + }); if (tradesPresenceCleaned) { - updateGridData(tradesPresenceCleaned) + updateGridData(tradesPresenceCleaned); } - } + }; - startOfferPresenceMapping() - } + startOfferPresenceMapping(); + }; const restartTradeOffersWebSocket = () => { - setTimeout(() => initTradeOffersWebSocket(true), 50) - } + setTimeout(() => initTradeOffersWebSocket(true), 50); + }; const initTradePresenceWebSocket = (restarted = false) => { - let socketTimeout: any - let socketLink - if(isUsingGateway){ - socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradepresence` + if (socketPresenceRef.current) return; + let socketTimeout: any; + let socketLink; + if (isUsingGateway) { + socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradepresence`; } else { - socketLink = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websockets/crosschain/tradepresence`; + socketLink = `${ + window.location.protocol === "https:" ? "wss:" : "ws:" + }//${baseLocalHost}/websockets/crosschain/tradepresence`; + } - } - - - const socket = new WebSocket(socketLink) - socket.onopen = () => { - setTimeout(pingSocket, 50) - } - socket.onmessage = (e) => { - tradePresenceTxns.current = JSON.parse(e.data) - processOffersWithPresence() - restarted = false - } - socket.onclose = () => { - clearTimeout(socketTimeout) - restartTradePresenceWebSocket() - } - socket.onerror = (e) => { - clearTimeout(socketTimeout) - } + socketPresenceRef.current = new WebSocket(socketLink); + socketPresenceRef.current.onopen = () => { + setTimeout(pingSocket, 50); + }; + socketPresenceRef.current.onmessage = (e) => { + tradePresenceTxns.current = !initiatedFetchPresenceSocket.current + ? JSON.parse(e.data) + : [...tradePresenceTxns.current, ...JSON.parse(e.data)]; + initiatedFetchPresenceSocket.current = true; + processOffersWithPresence(); + restarted = false; + }; + socketPresenceRef.current.onclose = (event) => { + clearTimeout(socketTimeout); + if (event.reason === "forced") { + return; + } + restartTradePresenceWebSocket(); + }; + socketPresenceRef.current.onerror = (e) => { + clearTimeout(socketTimeout); + restartTradePresenceWebSocket(); + }; const pingSocket = () => { - socket.send('ping') - socketTimeout = setTimeout(pingSocket, 295000) - } - } + socketPresenceRef.current.send("ping"); + socketTimeout = setTimeout(pingSocket, 295000); + }; + }; const initTradeOffersWebSocket = (restarted = false) => { - let tradeOffersSocketCounter = 0 - let socketTimeout: any + if (socketRef.current) return; + let socketTimeout: any; - let socketLink - if(isUsingGateway){ - socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradeoffers?foreignBlockchain=LITECOIN&includeHistoric=true` + let socketLink; + if (isUsingGateway) { + socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true`; } else { - socketLink = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websockets/crosschain/tradeoffers?foreignBlockchain=LITECOIN&includeHistoric=true` - - } - const socket = new WebSocket(socketLink) - socket.onopen = () => { - setTimeout(pingSocket, 50) - tradeOffersSocketCounter += 1 - } - socket.onmessage = (e) => { - offeringTrades.current = [...offeringTrades.current, ...JSON.parse(e.data)] - tradeOffersSocketCounter += 1 - restarted = false - processOffersWithPresence() - } - socket.onclose = () => { - clearTimeout(socketTimeout) - restartTradeOffersWebSocket() - } - socket.onerror = (e) => { - clearTimeout(socketTimeout) + socketLink = `${ + window.location.protocol === "https:" ? "wss:" : "ws:" + }//${baseLocalHost}/websockets/crosschain/tradeoffers?foreignBlockchain=${selectedCoin}&includeHistoric=true`; } + socketRef.current = new WebSocket(socketLink); + socketRef.current.onopen = () => { + setTimeout(pingSocket, 50); + }; + socketRef.current.onmessage = (e) => { + offeringTrades.current = [ + ...offeringTrades.current?.filter( + (coin) => coin?.foreignBlockchain === selectedCoin + ), + ...JSON.parse(e.data)?.filter( + (coin) => coin?.foreignBlockchain === selectedCoin + ), + ]; + restarted = false; + processOffersWithPresence(); + }; + socketRef.current.onclose = (event) => { + clearTimeout(socketTimeout); + if (event.reason === "forced") { + return; + } + restartTradeOffersWebSocket(); + socketRef.current = null; + }; + socketRef.current.onerror = (e) => { + clearTimeout(socketTimeout); + }; const pingSocket = () => { - socket.send('ping') - socketTimeout = setTimeout(pingSocket, 295000) - } - } - + socketRef.current.send("ping"); + socketTimeout = setTimeout(pingSocket, 295000); + }; + }; useEffect(() => { - blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]') - initTradePresenceWebSocket() - initTradeOffersWebSocket() - getNewBlockedTrades() + blockedTradesList.current = JSON.parse( + localStorage.getItem("failedTrades") || "[]" + ); + if (!initiatedFetchPresence.current) { + initiatedFetchPresence.current = true; + initTradePresenceWebSocket(); + } + getNewBlockedTrades(); const intervalBlockTrades = setInterval(() => { - getNewBlockedTrades() - }, 150000) + initiatedFetchPresenceSocket.current = false; + restartPresence(); + initTradePresenceWebSocket(); + getNewBlockedTrades(); + }, 150000); return () => { - clearInterval(intervalBlockTrades) - } - }, [isUsingGateway]) - + clearInterval(intervalBlockTrades); + }; + }, [isUsingGateway]); + useEffect(() => { + if (selectedCoin === null) return; + restartTradeOffers(); + setTimeout(() => { + initTradeOffersWebSocket(); + }, 500); + return () => { + if (socketRef.current) { + socketRef.current.close(1000, "forced"); + } + }; + }, [isUsingGateway, selectedCoin]); const selectedTotalLTC = useMemo(() => { return selectedOffers.reduce((acc: number, curr: any) => { @@ -310,47 +484,60 @@ export const TradeOffers: React.FC = ({ltcBalance}:any) => { }, 0); }, [selectedOffers]); - const buyOrder = async () => { try { - if(+ltcBalance < +selectedTotalLTC.toFixed(4)){ - setOpen(true) + if (+foreignCoinBalance < +selectedTotalLTC.toFixed(4)) { + setOpen(true); setInfo({ - type: 'error', - message: "You don't have enough LTC or your balance was not retrieved" - }) - return + type: "error", + message: `You don't have enough ${getCoinLabel()} or your balance was not retrieved`, + }); + return; } - - if (selectedOffers?.length < 1) return - setOpen(true) - setInfo({ - type: 'info', - message: "Attempting to submit buy order. Please wait..." - }) - const listOfATs = selectedOffers - const response = await qortalRequestWithTimeout({ - action: "CREATE_TRADE_BUY_ORDER", - crosschainAtInfo: listOfATs, - foreignBlockchain: 'LITECOIN' - }, 900000); - - if(response?.error){ - setOpen(true) - setInfo({ - type: 'error', - message: response?.error || "Failed to submit trade order." - }) - return + if (selectedOffers?.length < 1) return; + + setIsShowBuyInProgress({ status: "buying" }); + + // setOpen(true) + // setInfo({ + // type: 'info', + // message: "Attempting to submit buy order. Please wait..." + // }) + const listOfATs = selectedOffers; + const response = await qortalRequestWithTimeout( + { + action: "CREATE_TRADE_BUY_ORDER", + crosschainAtInfo: listOfATs, + foreignBlockchain: selectedCoin, + }, + 900000 + ); + + if (response?.error) { + setIsShowBuyInProgress({ + status: "error", + message: response?.error || "Failed to submit trade order.", + }); + // setOpen(true) + // setInfo({ + // type: 'error', + // message: response?.error || "Failed to submit trade order." + // }) + return; } if (response?.extra?.atAddresses) { - setSelectedOffers([]) + setIsShowBuyInProgress({ status: "success" }); + setSelectedOffers([]); const transactionData = { qortalAtAddresses: response?.extra?.atAddresses, qortAddress: response?.extra?.senderAddress, node: response?.extra?.node, - status:response?.extra?.status ? response?.extra?.status : response.callResponse === true ? 'trade-ongoing' : 'trade-failed', + status: response?.extra?.status + ? response?.extra?.status + : response.callResponse === true + ? "trade-ongoing" + : "trade-failed", encryptedMessageToBase58: response?.encryptedMessageToBase58, chatSignature: response?.chatSignature, sender: response?.extra?.senderAddress, @@ -358,43 +545,44 @@ export const TradeOffers: React.FC = ({ltcBalance}:any) => { reference: response?.callResponse?.reference, }; - - // Update transactions in IndexedDB const result = await updateTransactionInDB(transactionData); - - fetchOngoingTransactions() - if(isUsingGateway){ - setRecord(transactionData) + setOpen(true); + setInfo({ + type: "success", + message: "Submitted Order", + }); + fetchOngoingTransactions(); + if (isUsingGateway) { + setRecord(transactionData); await showInfo({ message: `Keep a record of your order in case your trade gets stuck`, - }) + }); } - setOpen(true) - setInfo({ - type: 'success', - message: "Submitted Order" - }) } - } catch (error) { - setOpen(true) - setInfo({ - type: 'error', - message: error?.message || "Failed to submit trade order." - }) - console.error(error) + setIsShowBuyInProgress({ + status: "error", + message: + error?.error || error?.message || "Failed to submit trade order.", + }); + // setOpen(true) + // setInfo({ + // type: 'error', + // message: error?.error || error?.message || "Failed to submit trade order." + // }) + // console.error(error) } - } + }; - - const getRowStyle = (params: RowClassParams): RowStyle | undefined => { - + const getRowStyle = ( + params: RowClassParams + ): RowStyle | undefined => { if (listOfOngoingTradesAts.includes(params.data.qortalAtAddress)) { - return { background: '#D9D9D91A'}; + return { background: "#D9D9D91A" }; } if (params.data.qortalAtAddress === selectedOffer?.qortalAtAddress) { - return { background: '#6D94F533'}; + return { background: "#6D94F533" }; } return undefined; }; @@ -405,143 +593,156 @@ export const TradeOffers: React.FC = ({ltcBalance}:any) => { const onSelectionChanged = (event: any) => { const selectedRows = event.api.getSelectedRows(); - + setSelectedOffers([...selectedRows]); // Set all selected rows }; - + const onRowClicked = (event: any) => { if (listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return; const selectedRows = gridRef.current?.api.getSelectedRows(); setSelectedOffers([...selectedRows]); // Always spread the array to ensure state updates correctly }; - const updateGridData = (newData: any) => { if (gridRef.current) { - setOffers(newData); - } }; const getRowId = useCallback(function (params: any) { return String(params.data.qortalAtAddress); -}, []); + }, []); -const selectedTotalQORT = useMemo(() => { - return selectedOffers.reduce((acc: number, curr: any) => { - return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined - }, 0); -}, [selectedOffers]); + const selectedTotalQORT = useMemo(() => { + return selectedOffers.reduce((acc: number, curr: any) => { + return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined + }, 0); + }, [selectedOffers]); + const onGridReady = useCallback((params: any) => { + params.api.sizeColumnsToFit(); // Adjust columns to fit the grid width + const allColumnIds = params.columnApi + .getAllColumns() + .map((col: any) => col.getColId()); + params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content + }, []); -const onGridReady = useCallback((params: any) => { - params.api.sizeColumnsToFit(); // Adjust columns to fit the grid width - const allColumnIds = params.columnApi.getAllColumns().map((col: any) => col.getColId()); - params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content -}, []); + const handleClose = ( + event?: React.SyntheticEvent | Event, + reason?: SnackbarCloseReason + ) => { + if (reason === "clickaway") { + return; + } - -const handleClose = ( - event?: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, -) => { - if (reason === 'clickaway') { - return; - } - - setOpen(false); - setInfo(null) -}; - - - + setOpen(false); + setInfo(null); + }; return ( - -
- params.data.qortalAtAddress} // Ensure rows have unique IDs - /> - {/* {selectedOffer && ( + +
+ params.data.qortalAtAddress} // Ensure rows have unique IDs + /> + {/* {selectedOffer && ( )} */} -
- - - {selectedTotalQORT?.toFixed(3)} QORT - - ltcBalance ? 'red' : 'white', - }}>{selectedTotalLTC?.toFixed(4)} LTC - - +
+
+ + + + + {selectedTotalQORT?.toFixed(3)} QORT + + + foreignCoinBalance ? "red" : "white", + }} + > + {selectedTotalLTC?.toFixed(4)}{" "} + {`${getCoinLabel()} `} + + + + {foreignCoinBalance?.toFixed(4)}{" "} + + {`${getCoinLabel()} `} balance + + - {ltcBalance?.toFixed(4)} LTC balance - - {BuyButton()} - - + {BuyButton()} + + {info?.message} @@ -554,22 +755,171 @@ const handleClose = ( > {"Download record"} - + {messageInfo.message} - + - )} - + {isShowBuyInProgress && ( + + + {isShowBuyInProgress?.status === "error" && ( + + + + {` Failed to submit buy order.`} + + + {isShowBuyInProgress?.message} + + )} + {isShowBuyInProgress?.status === "success" && ( + + + + {` Successfully submitted order.`} + + + + You can see the progress of your order in the "Pending" table. + + + + Note: Submission of an order does not necessarily mean that + your submission will be the one completing the order. Another + account may have submitted it before you. + + + )} + {isShowBuyInProgress?.status === "buying" && ( + + + + {` Attempting to submit buy order`} + + + + Please do not leave this application until there is a + response. Please wait! + + + {isUsingGateway && ( + <> + + Using gateway: might take up to 3 minutes to submit the + buy order. + + + + { + //nothing + }} + size={60} + strokeWidth={4} + > + {({ remainingTime }) => ( + {remainingTime} + )} + + + + )} + + )} + + + + + + )} + ); }; - diff --git a/src/components/Terms.tsx b/src/components/Terms.tsx index d1e31a4..3ab87f2 100644 --- a/src/components/Terms.tsx +++ b/src/components/Terms.tsx @@ -55,22 +55,22 @@ export const Terms =() => { - The purpose of qort.trade is to make trading LTC for QORT as easy as possible. The maintainers of this site do not profit from its use—there are no additional fees for buying QORT through this site. There are two ways to place a buy order: + The purpose of q-trade is to make trading LTC and other coins for QORT as easy as possible. The maintainers of this site do not profit from its use—there are no additional fees for buying QORT through this site. There are two ways to place a buy order: 1. Use the gateway 2. Use your local node. - By using qort.trade, you agree to the following terms and conditions. + By using q-trade, you agree to the following terms and conditions. - Using the gateway means you trust the maintainer of the node, as your LTC private key will need to be handled by that node to execute a trade order. If you have more than 4 QORT and your public key is already on the blockchain, your LTC private key will be transmitted using q-chat. If not, the message will be encrypted in the same manner as q-chat but stored temporarily in a database to ensure it reaches its destination. + Using the gateway means you trust the maintainer of the node, as your foreign coin (i.e. LTC) private key will need to be handled by that node to execute a trade order. If you have more than 4 QORT and your public key is already on the blockchain, your foreign coin private key will be transmitted using q-chat. If not, the message will be encrypted in the same manner as q-chat but stored temporarily in a database to ensure it reaches its destination. - If you are uncomfortable using the gateway, we offer the option to use your local node to buy QORT. When logging into the extension, choose the local node configuration, and use the switch button on qort.trade to connect with your local node. + If you are uncomfortable using the gateway, we offer the option to use your local node to buy QORT. When logging into the UI, choose the local node configuration. - The maintainers of this site are not responsible for any lost LTC, QORT, or other cryptocurrencies that may result from using this site. This is a hobby project, and mistakes in the code may occur. Please proceed with caution. + The maintainers and devs of this site are not responsible for any lost foreign coin, QORT, or other cryptocurrencies that may result from using this site. This is a hobby project, and mistakes in the code may occur. diff --git a/src/components/common/icons/qtradeLogo.png b/src/components/common/icons/qtradeLogo.png new file mode 100644 index 0000000..2735d38 Binary files /dev/null and b/src/components/common/icons/qtradeLogo.png differ diff --git a/src/components/common/reusable-modal/ReusableModal-styles.tsx b/src/components/common/reusable-modal/ReusableModal-styles.tsx index c9358b4..8ed0f36 100644 --- a/src/components/common/reusable-modal/ReusableModal-styles.tsx +++ b/src/components/common/reusable-modal/ReusableModal-styles.tsx @@ -1,5 +1,6 @@ import { Box, Button } from "@mui/material"; import { styled } from "@mui/system"; +import CloseIcon from '@mui/icons-material/Close'; export const ReusableModalContainer = styled(Box)(({ theme }) => ({ display: "flex", @@ -21,15 +22,6 @@ export const ReusableModalContainer = styled(Box)(({ theme }) => ({ "0px 4px 5px 0px hsla(0,0%,0%,0.14), \n\t\t0px 1px 10px 0px hsla(0,0%,0%,0.12), \n\t\t0px 2px 4px -1px hsla(0,0%,0%,0.2)", })); -export const ReusableModalSubContainer = styled(Box)({ - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - gap: "20px", - padding: "70px", -}); - export const ReusableModalBackdrop = styled(Box)({ position: "fixed", top: "0", @@ -50,4 +42,17 @@ export const ReusableModalButton = styled(Button)(({ theme }) => ({ border: `1px solid ${theme.palette.text.primary}`, color: theme.palette.text.primary, boxShadow: "1px 4px 10.5px 0px #0000004D" +})); + +export const ReusableModalCloseIcon = styled(CloseIcon)(({ theme }) => ({ + color: theme.palette.text.primary, + cursor: "pointer", + fontSize: "30px", + position: "absolute", + top: "20px", + right: "20px", + transition: "all 0.3s ease-in-out", + "&:hover": { + transform: "scale(1.1)", + }, })); \ No newline at end of file diff --git a/src/components/common/reusable-modal/ReusableModal.tsx b/src/components/common/reusable-modal/ReusableModal.tsx index d2b6d74..877b136 100644 --- a/src/components/common/reusable-modal/ReusableModal.tsx +++ b/src/components/common/reusable-modal/ReusableModal.tsx @@ -1,20 +1,26 @@ import { ReusableModalBackdrop, + ReusableModalCloseIcon, ReusableModalContainer, - ReusableModalSubContainer, } from "./ReusableModal-styles"; interface ReusableModalProps { backdrop?: boolean; + onClickClose: () => void; children: React.ReactNode; } -export const ReusableModal: React.FC = ({ backdrop, children }) => { +export const ReusableModal: React.FC = ({ + backdrop, + children, + onClickClose, +}) => { return ( <> - {children} + + {children} - {backdrop && } + {backdrop && } ); }; diff --git a/src/components/header/AddressQRCode.tsx b/src/components/header/AddressQRCode.tsx new file mode 100644 index 0000000..f4858fb --- /dev/null +++ b/src/components/header/AddressQRCode.tsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; +import QRCode from "react-qr-code"; +import { Box, Typography } from "@mui/material"; + +export const AddressQRCode = ({ targetAddress }) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/src/components/header/Header-styles.tsx b/src/components/header/Header-styles.tsx index 7938beb..d98d007 100644 --- a/src/components/header/Header-styles.tsx +++ b/src/components/header/Header-styles.tsx @@ -1,5 +1,5 @@ import { styled } from "@mui/system"; -import { Box, Typography } from "@mui/material"; +import { Box, Button, Typography, Theme, TextField } from "@mui/material"; import { HomeSVG } from "../common/icons/HomeSVG"; import { QortalLogoSVG } from "../common/icons/QortalLogoSVG"; import { CaretDownSVG } from "../common/icons/CaretDownSVG"; @@ -16,6 +16,14 @@ export const HeaderNav = styled(Box)(({ theme }) => ({ }, })); +export const BubbleCardColored1 = styled(Box)({ + height: "77px", + width: "77px", + background: "linear-gradient(124.49deg, #70BAFF 7.03%, #F29999 94.22%)", + boxShadow: "0px 0px 25.8px -1px #1C5A93", + borderRadius: "50%", +}); + export const HomeIcon = styled(HomeSVG)({ cursor: "pointer", }); @@ -110,7 +118,7 @@ export const GameSelectDropdownMenuItem = styled(Box)(({ theme }) => ({ export const Username = styled(Typography)(({ theme }) => ({ fontFamily: "Fira Sans, sans-serif", - fontSize: "16px", + fontSize: "20px", lineHeight: "19.2px", fontWeight: 400, color: theme.palette.text.primary, @@ -130,13 +138,14 @@ export const LogoColumn = styled(Box)({ gap: "10px", alignItems: "center", }); + export const RightColumn = styled(Box)({ display: "flex", flexDirection: "row", gap: "10px", alignItems: "flex-start", - padding: '10px' }); + export const AvatarCircle = styled("img")({ borderRadius: "50%", width: "35px", @@ -145,12 +154,188 @@ export const AvatarCircle = styled("img")({ userSelect: "none", }); - export const HeaderText = styled(Typography)(({ theme }) => ({ fontFamily: "Inter", color: theme.palette.text.primary, + textAlign: "center", fontWeight: 500, fontSize: "16px", lineHeight: 1.2, userSelect: "none", })); + +export const TotalCol = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "5px", +}); + +export const CoinActionsRow = styled(Box)({ + display: "flex", + flexDirection: "row", + gap: "5px", + alignItems: "center", + justifyContent: "center", +}); + +export const CoinSendBtn = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + color: "#000000", + border: `1px solid ${theme.palette.primary.main}`, + fontFamily: "Inter, sans-serif", + fontWeight: 500, + fontSize: "14px", + lineHeight: "16px", + padding: "5px 10px", + borderRadius: "0px", + transition: "all 0.3s ease-in-out", + "&:hover": { + border: `1px solid ${theme.palette.text.primary}`, + backgroundColor: theme.palette.text.primary, + }, +})); + +export const CoinReceiveBtn = styled(Button)(({ theme }) => ({ + backgroundColor: "transparent", + color: theme.palette.text.primary, + border: `1px solid ${theme.palette.text.primary}`, + fontFamily: "Inter, sans-serif", + fontWeight: 500, + fontSize: "14px", + lineHeight: "16px", + padding: "5px 10px", + borderRadius: "0px", + transition: "all 0.3s ease-in-out", + "&:hover": { + color: "#000000", + backgroundColor: theme.palette.text.primary, + }, +})); + +export const CoinSelectRow = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "5px", + alignSelf: "flex-start", + marginBottom: "5px" +}); + +export const CoinActionContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + gap: "25px", + alignItems: "center", + justifyContent: "center", + width: "80%", +}); + +export const CoinActionRow = styled(Box)({ + display: "flex", + flexDirection: "row", + width: "100%", + alignItems: "center", + justifyContent: "center", +}); + +export const HeaderRow = styled(Box)({ + display: "flex", + flexDirection: "row", + gap: "10px", + alignItems: "center", + justifyContent: "center", +}); + +export const SendFont = styled(Typography)(({ theme }) => ({ + fontFamily: "Inter", + color: theme.palette.text.primary, + fontWeight: 400, + fontSize: "18px", + lineHeight: "25px", + userSelect: "none", +})); + +const customInputStyle = (theme: Theme) => { + return { + fontFamily: "Inter", + fontSize: "18px", + fontWeight: 300, + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.default, + borderColor: theme.palette.background.paper, + "& label": { + color: theme.palette.mode === "light" ? "#808183" : "#edeef0", + fontFamily: "Inter", + fontSize: "18px", + letterSpacing: "0px", + }, + "& label.Mui-focused": { + color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da", + }, + "& .MuiInput-underline:after": { + borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf", + }, + "& .MuiOutlinedInput-root": { + "& fieldset": { + borderColor: "#E0E3E7", + }, + "&:hover fieldset": { + borderColor: "#B2BAC2", + }, + "&.Mui-focused fieldset": { + borderColor: "#6F7E8C", + }, + }, + "& .MuiInputBase-root": { + fontFamily: "Inter", + fontSize: "18px", + letterSpacing: "0px", + }, + "& .MuiFilledInput-root:after": { + borderBottomColor: theme.palette.secondary.main, + }, + }; +}; + +export const CustomInputField = styled(TextField)(({ theme }) => + customInputStyle(theme as Theme) +); + +export const CoinCancelBtn = styled(Button)({ + backgroundColor: "transparent", + color: "#d62525", + border: "1px solid #d62525", + fontFamily: "Inter, sans-serif", + fontWeight: 500, + fontSize: "14px", + height: "32px", + width: "80px", + lineHeight: "16px", + padding: "5px 10px", + borderRadius: "0px", + transition: "all 0.3s ease-in-out", + "&:hover": { + border: "1px solid #d62525", + backgroundColor: "#d62525", + color: "#000000", + }, +}); + +export const CoinConfirmSendBtn = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + color: "#000000", + border: `1px solid ${theme.palette.primary.main}`, + fontFamily: "Inter, sans-serif", + fontWeight: 500, + fontSize: "14px", + height: "32px", + width: "80px", + lineHeight: "16px", + padding: "5px 10px", + borderRadius: "0px", + transition: "all 0.3s ease-in-out", + "&:hover": { + border: `1px solid ${theme.palette.text.primary}`, + color: "#000000", + backgroundColor: theme.palette.text.primary, + }, +})); diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index 6ac5ad7..d22a16b 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -1,41 +1,63 @@ -import { useState, useEffect, useRef, useContext, ChangeEvent } from "react"; -import ReactGA from "react-ga4"; +import { useState, useEffect, useRef, useContext, ChangeEvent, useMemo } from "react"; import { - AvatarCircle, - CaretDownIcon, - DropdownContainer, - GameSelectDropdown, - GameSelectDropdownMenu, - GameSelectDropdownMenuItem, + BubbleCardColored1, + CoinActionContainer, + CoinActionRow, + CoinActionsRow, + CoinCancelBtn, + CoinConfirmSendBtn, + CoinReceiveBtn, + CoinSelectRow, + CoinSendBtn, + CustomInputField, HeaderNav, + HeaderRow, HeaderText, - HomeIcon, LogoColumn, NameRow, - QortalLogoIcon, RightColumn, + SendFont, + TotalCol, Username, } from "./Header-styles"; import gameContext from "../../contexts/gameContext"; import { UserContext } from "../../contexts/userContext"; import { cropAddress } from "../../utils/cropAddress"; -import { BubbleCardColored1 } from "../../pages/Home/Home-Styles"; -import logoSVG from "../../assets/SVG/LOGO.svg"; +import qtradeLogo from "../../components/common/icons/qtradeLogo.png"; +import qortIcon from "../../assets/img/qort.png"; +import ErrorIcon from "@mui/icons-material/Error"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import Copy from "../../assets/SVG/Copy.svg"; +import {AddressQRCode} from './AddressQRCode' +import {FallingLines} from 'react-loader-spinner' import { Alert, + AppBar, Avatar, Box, - Button, + Card, + CardContent, + FormControl, FormControlLabel, MenuItem, Select, Snackbar, SnackbarCloseReason, Switch, + Typography, styled, } from "@mui/material"; import { sendRequestToExtension } from "../../App"; import { Terms } from "../Terms"; +import ltcIcon from "../../assets/img/ltc.png"; +import btcIcon from "../../assets/img/btc.png"; +import dogeIcon from "../../assets/img/doge.png"; +import rvnIcon from "../../assets/img/rvn.png"; +import dgbIcon from "../../assets/img/dgb.png"; +import arrrIcon from "../../assets/img/arrr.png"; +import { Spacer } from "../common/Spacer"; +import { ReusableModal } from "../common/reusable-modal/ReusableModal"; +import { NotificationContext } from "../../contexts/notificationContext"; const checkIfLocal = async () => { try { @@ -59,15 +81,79 @@ export const Label = styled("label")( ` ); -export const Header = ({ qortBalance, ltcBalance, mode, setMode }: any) => { +interface CoinModalProps { + coin: string; + type: string; +} + +const getCoinIcon = (coin) => { + let img; + + switch (coin) { + case "LTC": + img = ltcIcon; + break; + case "BTC": + img = btcIcon; + break; + + case "DOGE": + img = dogeIcon; + break; + case "RVN": + img = rvnIcon; + break; + + case "ARRR": + img = arrrIcon; + break; + case "DGB": + img = dgbIcon; + break; + default: + null; + } + return img; +}; + +const SelectRow = ({ coin }) => { + let img = getCoinIcon(coin); + + return ( +
+ +

{coin}

+
+ ); +}; + +export const Header = ({ qortBalance, foreignCoinBalance }: any) => { const [openDropdown, setOpenDropdown] = useState(false); const dropdownRef = useRef(null); const buttonRef = useRef(null); const [checked, setChecked] = useState(false); const [open, setOpen] = useState(false); const [info, setInfo] = useState(null); + const [openCoinActionModal, setOpenCoinActionModal] = + useState(null); + const [receiverAddress, setReceiverAddress] = useState(""); + const [senderAddress, setSenderAddress] = useState(""); + const [amount, setAmount] = useState(""); + const [coinAddresses, setCoinAddresses] = useState({}); const { isUsingGateway } = useContext(gameContext); - const [selectedCoin, setSelectedCoin] = useState("LITECOIN"); const handleChange = (event: ChangeEvent) => { setChecked(false); @@ -77,8 +163,10 @@ export const Header = ({ qortBalance, ltcBalance, mode, setMode }: any) => { message: "Change the node you are using at the authentication page", }); }; - const { userInfo } = useContext(gameContext); - const { avatar, setAvatar } = useContext(UserContext); + const { userInfo, selectedCoin, setSelectedCoin, getCoinLabel } = + useContext(gameContext); + const { setNotification } = useContext(NotificationContext); + const LocalNodeSwitch = styled(Switch)(({ theme }) => ({ padding: 8, @@ -151,119 +239,510 @@ export const Header = ({ qortBalance, ltcBalance, mode, setMode }: any) => { // } // }, [userInfo]); + const sendCoin = async ()=> { + try { + const coin = openCoinActionModal.coin === "QORT" ? 'QORT' : getCoinLabel() + if(!coin) return + setOpen(true); + setInfo({ + type: "info", + message: "Sending Coin...", + autoHideDurationOff: true + }); + const response = await qortalRequest({ + action: "SEND_COIN", + coin, + destinationAddress: senderAddress, + amount: +amount + }); + if(response?.error){ + throw new Error(response?.error || "Failed to send coin.") + } + setOpen(true); + setInfo({ + type: "success", + message: "Coin sent", + }); + setAmount('') + } catch (error) { + setOpen(true); + setInfo({ + type: "error", + message: error?.error || error?.message, + }); + } + } + return ( - - + - - - + + + + + } + label="Is using Gateway" + /> + + + + + + + {userInfo?.name ? ( + + {userInfo?.name?.charAt(0)?.toUpperCase()} + + ) : userInfo?.address ? ( + + ) : null} + {userInfo?.name ? ( + {userInfo?.name} + ) : userInfo?.address ? ( + {cropAddress(userInfo?.address)} + ) : null} + + + + + + + + Total Balance + + + + + {qortBalance} QORT + + + { + setOpenCoinActionModal({ + coin: "QORT", + type: "send", + }); + }} + > + Send + + { + setOpenCoinActionModal({ + coin: "QORT", + type: "receive", + }); + }} + > + Receive + + + + + + + + {foreignCoinBalance === null ? ( + + ) : foreignCoinBalance}{" "} + {getCoinLabel()} + + + { + setOpenCoinActionModal({ + coin: selectedCoin, + type: "send", + }); + }} + > + Send + + { + setOpenCoinActionModal({ + coin: selectedCoin, + type: "receive", + }); + }} + > + Receive + + + + + + + + - - - - - - Balance: {qortBalance} QORT |{" "} - {ltcBalance === null ? "N/A" : ltcBalance} LTC - - - {userInfo?.name ? ( - {userInfo?.name} - ) : userInfo?.address ? ( - {cropAddress(userInfo?.address)} - ) : null} - - {userInfo?.name ? ( - - {userInfo?.name?.charAt(0)?.toUpperCase()} - - ) : userInfo?.address ? ( - - ) : ( - { - window.open("https://www.qortal.dev", "_blank")?.focus(); - }} - /> - )} - - - - } - label="Is using Gateway" - /> - - - - - - - - + - {info?.message} - - - + {info?.type && ( + + {info?.message} + + )} + +
+ {openCoinActionModal && ( + { + setOpenCoinActionModal(null); + setAmount('') + setSenderAddress('') + }} + backdrop + > + + {openCoinActionModal.type === "send" ? <> + + + {openCoinActionModal.type === "send" && + openCoinActionModal.coin === "QORT" ? ( + <> + Send {openCoinActionModal.coin} + + + ) : openCoinActionModal.type === "send" && + openCoinActionModal.coin !== "QORT" ? ( + <> + Send {openCoinActionModal.coin} + + + ) : null} + + + + + { + if (openCoinActionModal.type === "send") { + setSenderAddress(e.target.value); + } else { + setReceiverAddress(e.target.value); + } + }} + /> + + + {openCoinActionModal.type === "send" && ( + + + { + setAmount(e.target.value) + }} + /> + + + )} + : ( + <> + + + )} + {openCoinActionModal.type === 'send' && ( + + {/* setOpenCoinActionModal(null)}> + Cancel + */} + { + if(openCoinActionModal.type === 'send'){ + sendCoin() + } + setNotification({ + alertType: "alertInfo", + msg: "Sending...", + }); + }} + > + {openCoinActionModal.type === "send" ? "Send" : "Receive"} + + + )} + + + + + + )} + + + ); }; + +export const AddressBox = styled(Box)` +display: flex; +border: 1px solid var(--50-white, rgba(255, 255, 255, 0.5)); +justify-content: space-between; +align-items: center; +width: auto; +word-break: break-word; +padding: 5px 15px 5px 15px; +gap: 5px; +border-radius: 100px; +font-family: Inter; +font-size: 12px; +font-weight: 600; +line-height: 14.52px; +text-align: left; +color: var(--50-white, rgba(255, 255, 255, 0.5)); +cursor: pointer; +transition: all 0.2s; +&:hover { + background-color: rgba(41, 41, 43, 1); + color: white; + svg path { + fill: white; // Fill color changes to white on hover + } + } + +` + + +const ReceiveCoin = ({coinAddresses, setCoinAddresses, selectedCoin, setOpen, setInfo})=> { + const [errorMsg, setErrorMsg] = useState('') + const foreignAddress = useMemo(()=> { + return coinAddresses[selectedCoin] || null + }, [coinAddresses, selectedCoin]) + + const getForeignAddress = async (coin)=> { + try { + setOpen(true); + setInfo({ + type: "info", + message: "Retrieving address...", + }); + const response = await qortalRequest({ + action: "GET_USER_WALLET", + coin + }); + if(response?.address){ + setCoinAddresses((prev)=> { + return { + ...prev, + [coin]: response.address + } + }) + } + if(response?.error){ + throw new Error(response?.error || "Failed to send coin.") + } + } catch (error) { + setErrorMsg(error?.message) + } finally { + setOpen(false); + setInfo(null); + } + } + + useEffect(()=> { + if(!selectedCoin) return + if(!coinAddresses[selectedCoin]){ + getForeignAddress(selectedCoin) + } + }, [selectedCoin, coinAddresses]) + + return ( + + {`Send ${selectedCoin} to your address below`} + + {foreignAddress && ( + { + setOpen(true); + setInfo({ + type: "info", + message: "Address copied!", + }); + }}> + + {foreignAddress} + + + )} + {foreignAddress && ( + <> + + + )} + {errorMsg && ( + <> + + {errorMsg} + + )} + + ) +} \ No newline at end of file diff --git a/src/components/history/History-styles.tsx b/src/components/history/History-styles.tsx new file mode 100644 index 0000000..7950943 --- /dev/null +++ b/src/components/history/History-styles.tsx @@ -0,0 +1,57 @@ +import { Box, styled } from "@mui/system"; +import { Button, Typography } from "@mui/material"; +import RefreshIcon from "@mui/icons-material/Refresh"; + +type HistoryBtnProp = { + activeBtn: boolean; +}; + +export const HistoryButtonRow = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "5px", + margin: "5px 5px 5px 0", +}); + +export const HistoryButton = styled(Button, { + shouldForwardProp: (prop) => prop !== "activeBtn", +})(({ theme, activeBtn }) => ({ + fontFamily: "Inter", + color: activeBtn ? theme.palette.text.primary : theme.palette.primary.main, + fontWeight: 400, + fontSize: "16px", + height: "30px", + lineHeight: "40px", + userSelect: "none", + background: activeBtn ? theme.palette.primary.main : "transparent", + border: `1px solid ${theme.palette.primary.main}`, + transition: "all 0.3s ease-in-out", + "&:hover": { + border: `1px solid ${theme.palette.primary.main}`, + background: theme.palette.primary.main, + color: theme.palette.text.primary, + cursor: "pointer", + }, +})); + +export const Refresh = styled(RefreshIcon)({ + cursor: "pointer", + color: "#fff", + fontSize: "25px", + marginLeft: "5px", + transition: "all 0.3s ease-in-out", + "&:hover": { + cursor: "pointer", + transform: "scale(1.1)", + }, +}); + +export const ShowingFont = styled(Typography)(({ theme }) => ({ + fontFamily: "Inter", + color: theme.palette.text.primary, + fontWeight: 400, + fontSize: "16px", + lineHeight: "25px", + marginBottom: "5px", + userSelect: "none", +})); diff --git a/src/components/history/History.tsx b/src/components/history/History.tsx new file mode 100644 index 0000000..cf039e9 --- /dev/null +++ b/src/components/history/History.tsx @@ -0,0 +1,131 @@ +import { + Alert, + Box, + ButtonBase, + Snackbar +} from "@mui/material"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import gameContext from "../../contexts/gameContext"; +import HistoryList from "./HistoryList"; +import { ShowingFont, Refresh, HistoryButtonRow, HistoryButton } from "./History-styles"; + +export const History = ({ qortAddress, show }) => { + const [buyHistory, setBuyHistory] = useState({}); + const [sellHistory, setSellHistory] = useState({}); + + const { selectedCoin } = useContext(gameContext); + const [mode, setMode] = useState("buyHistory"); + const [open, setOpen] = useState(false); + + const selectedHistory = useMemo(() => { + if (mode === "buyHistory") return buyHistory[selectedCoin] || []; + if (mode === "sellHistory") return sellHistory[selectedCoin] || []; + }, [selectedCoin, buyHistory, sellHistory, mode]); + const getBuyHistory = useCallback( + (address, foreignBlockchain, mode, limit = 20) => { + setOpen(true); + let historyUrl; + if (mode === "buyHistory") { + historyUrl = `/crosschain/trades?foreignBlockchain=${foreignBlockchain}&buyerAddress=${address}&limit=${limit}&reverse=true`; + } + if (mode === "sellHistory") { + historyUrl = `/crosschain/trades?foreignBlockchain=${foreignBlockchain}&sellerAddress=${address}&limit=${limit}&reverse=true`; + } + + fetch(historyUrl) + .then((response) => { + return response.json(); + }) + .then((data) => { + if (mode === "buyHistory") { + setBuyHistory((prev) => { + return { + ...prev, + [foreignBlockchain]: data, + }; + }); + } + if (mode === "sellHistory") { + setSellHistory((prev) => { + return { + ...prev, + [foreignBlockchain]: data, + }; + }); + } + }) + .catch(() => {}) + .finally(() => { + setOpen(false); + }); + }, + [] + ); + + useEffect(() => { + if (!qortAddress || !selectedCoin) return; + if (mode === "buyHistory" && buyHistory[selectedCoin]) return; + if (mode === "sellHistory" && sellHistory[selectedCoin]) return; + + getBuyHistory(qortAddress, selectedCoin, mode); + }, [qortAddress, selectedCoin, buyHistory, mode]); + + return ( + + + { + setMode("buyHistory"); + }} + > + Buy History + + { + setMode("sellHistory"); + }} + > + Sell History + + { + getBuyHistory(qortAddress, selectedCoin, mode); + }} + > + + + + Showing most recent 20 results + + { + setOpen(false); + }} + > + setOpen(false)} + severity="info" + variant="filled" + sx={{ width: "100%" }} + > + {"Fetching History"} + + + + ); +}; diff --git a/src/components/history/HistoryList.tsx b/src/components/history/HistoryList.tsx new file mode 100644 index 0000000..4da4fd4 --- /dev/null +++ b/src/components/history/HistoryList.tsx @@ -0,0 +1,189 @@ +import { ColDef } from "ag-grid-community"; +import { AgGridReact } from "ag-grid-react"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { autoSizeStrategy, baseLocalHost } from "../Grids/TradeOffers"; +import { Alert, Box, Snackbar, SnackbarCloseReason, Typography } from "@mui/material"; +import gameContext from "../../contexts/gameContext"; +import { formatTimestampForum } from "../../utils/formatTime"; + +const defaultColDef = { + resizable: true, // Make columns resizable by default + sortable: true, // Make columns sortable by default + suppressMovable: true, // Prevent columns from being movable +}; + + + +export default function HistoryList({ qortAddress, historyList }) { + const gridRef = useRef(null); + const { getCoinLabel, selectedCoin} = useContext(gameContext) + const [qortalNames, setQortalNames] = useState({}); + + + const onGridReady = useCallback((params: any) => { + params.api.sizeColumnsToFit(); // Adjust columns to fit the grid width + const allColumnIds = params.columnApi + .getAllColumns() + .map((col: any) => col.getColId()); + params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content + }, []); + + const getName = async (address) => { + try { + const response = await fetch("/names/address/" + address); + const nameData = await response.json(); + if (nameData?.length > 0) { + setQortalNames((prev) => { + return { + ...prev, + [address]: nameData[0].name, + }; + }); + } else { + setQortalNames((prev) => { + return { + ...prev, + [address]: null, + }; + }); + } + } catch (error) { + // error + } + }; + + + const columnDefs: ColDef[] = useMemo(()=> { + return [ + { + headerName: "QORT AMOUNT", + field: "qortAmount", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: `${getCoinLabel()}/QORT`, + valueGetter: (params) => + +params.data.foreignAmount / +params.data.qortAmount, + sortable: true, + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: `Total ${getCoinLabel()} Value`, + field: "foreignAmount", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: "Time", + field: "tradeTimestamp", + valueGetter: (params) => + formatTimestampForum(params.data.tradeTimestamp), + flex: 1, // Flex makes this column responsive + minWidth: 200, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: "Buyer", + field: "buyerReceivingAddress", + flex: 1, // Flex makes this column responsive + minWidth: 200, // Ensure it doesn't shrink too much + resizable: true, + valueGetter: (params) => { + if (params?.data?.buyerReceivingAddress) { + if (qortalNames[params?.data?.buyerReceivingAddress]) { + return qortalNames[params?.data?.buyerReceivingAddress]; + } else if (qortalNames[params?.data?.buyerReceivingAddress] === undefined) { + getName(params?.data?.buyerReceivingAddress); + + return params?.data?.buyerReceivingAddress; + } else { + return params?.data?.buyerReceivingAddress; + } + } + }, + }, + { + headerName: "Seller", + field: "sellerAddress", + flex: 1, // Flex makes this column responsive + minWidth: 200, // Ensure it doesn't shrink too much + resizable: true, + valueGetter: (params) => { + if (params?.data?.sellerAddress) { + if (qortalNames[params?.data?.sellerAddress]) { + return qortalNames[params?.data?.sellerAddress]; + } else if (qortalNames[params?.data?.sellerAddress] === undefined) { + getName(params?.data?.sellerAddress); + + return params?.data?.sellerAddress; + } else { + return params?.data?.sellerAddress; + } + } + }, + }, + ]; + + }, [selectedCoin, qortalNames, getCoinLabel]) + + + + // const onSelectionChanged = (event: any) => { + // const selectedRows = event.api.getSelectedRows(); + // if(selectedRows[0]){ + // setSelectedTrade(selectedRows[0]) + // } else { + // setSelectedTrade(null) + // } + // }; + + + + + + + + return ( +
+
+ params.data.qortalAtAddress} // Ensure rows have unique IDs + /> +
+
+ ); +} diff --git a/src/components/sell/CreateSell.tsx b/src/components/sell/CreateSell.tsx index 814b877..3c6e471 100644 --- a/src/components/sell/CreateSell.tsx +++ b/src/components/sell/CreateSell.tsx @@ -1,10 +1,24 @@ -import { Alert, Box, Button, DialogActions, DialogContent, DialogTitle, IconButton, InputLabel, Snackbar, SnackbarCloseReason, TextField, Typography, styled } from '@mui/material' -import React, { useContext } from 'react' -import { BootstrapDialog } from '../Terms' -import CloseIcon from '@mui/icons-material/Close'; -import { Spacer } from '../common/Spacer'; -import gameContext from '../../contexts/gameContext'; -import TradeBotList from './TradeBotList'; +import { + Alert, + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + InputLabel, + Snackbar, + SnackbarCloseReason, + TextField, + Typography, + styled, +} from "@mui/material"; +import React, { useContext } from "react"; +import { BootstrapDialog } from "../Terms"; +import CloseIcon from "@mui/icons-material/Close"; +import { Spacer } from "../common/Spacer"; +import gameContext from "../../contexts/gameContext"; +import TradeBotList from "./TradeBotList"; export const CustomLabel = styled(InputLabel)` font-weight: 400; @@ -12,188 +26,223 @@ export const CustomLabel = styled(InputLabel)` font-size: 10px; line-height: 12px; color: rgba(255, 255, 255, 0.5); - -` +`; export const minimumAmountSellTrades = { - 'LITECOIN': { + LITECOIN: { value: 0.01, - ticker: 'LTC' - } -} + ticker: "LTC", + }, + DOGECOIN: { + value: 1, + ticker: "DOGE", + }, + BITCOIN: { + value: 0.001, + ticker: "BTC", + }, + DIGIBYTE: { + value: 0.01, + ticker: "DGB", + }, + RAVENCOIN: { + value: 0.01, + ticker: "RVN", + }, + PIRATECHAIN: { + value: 0.0002, + ticker: "ARRR", + }, +}; export const CustomInput = styled(TextField)({ - width: "183px", // Adjust the width as needed - borderRadius: "5px", - // backgroundColor: "rgba(30, 30, 32, 1)", + width: "183px", // Adjust the width as needed + borderRadius: "5px", + // backgroundColor: "rgba(30, 30, 32, 1)", + outline: "none", + input: { + fontSize: 10, + fontFamily: "Inter", + fontWeight: 400, + color: "white", + "&::placeholder": { + fontSize: 16, + color: "rgba(255, 255, 255, 0.2)", + }, outline: "none", - input: { - fontSize: 10, - fontFamily: "Inter", - fontWeight: 400, - color: "white", - "&::placeholder": { - fontSize: 16, - color: "rgba(255, 255, 255, 0.2)", - }, - outline: "none", - padding: "10px", + padding: "10px", + }, + "& .MuiOutlinedInput-root": { + "& fieldset": { + border: "0.5px solid rgba(255, 255, 255, 0.5)", }, - "& .MuiOutlinedInput-root": { - "& fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - "&:hover fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, - "&.Mui-focused fieldset": { - border: '0.5px solid rgba(255, 255, 255, 0.5)', - }, + "&:hover fieldset": { + border: "0.5px solid rgba(255, 255, 255, 0.5)", }, - "& .MuiInput-underline:before": { - borderBottom: "none", + "&.Mui-focused fieldset": { + border: "0.5px solid rgba(255, 255, 255, 0.5)", }, - "& .MuiInput-underline:hover:not(.Mui-disabled):before": { - borderBottom: "none", - }, - "& .MuiInput-underline:after": { - borderBottom: "none", - }, - }); - + }, + "& .MuiInput-underline:before": { + borderBottom: "none", + }, + "& .MuiInput-underline:hover:not(.Mui-disabled):before": { + borderBottom: "none", + }, + "& .MuiInput-underline:after": { + borderBottom: "none", + }, +}); -export const CreateSell = ({qortAddress, show}) => { - const [open, setOpen] = React.useState(false); - const [qortAmount, setQortAmount] = React.useState(0) - const [foreignAmount, setForeignAmount] = React.useState(0) - const {updateTemporaryFailedTradeBots, sellOrders, fetchTemporarySellOrders, isUsingGateway} = useContext(gameContext) - const [openAlert, setOpenAlert] = React.useState(false) - const [info, setInfo] = React.useState(null) - const handleClickOpen = () => { - setOpen(true); - }; - const handleClose = () => { - setOpen(false); - setForeignAmount(0) - setQortAmount(0) - }; +export const CreateSell = ({ qortAddress, show }) => { + const [open, setOpen] = React.useState(false); + const [qortAmount, setQortAmount] = React.useState(0); + const [foreignAmount, setForeignAmount] = React.useState(0); + const { + updateTemporaryFailedTradeBots, + sellOrders, + fetchTemporarySellOrders, + isUsingGateway, + getCoinLabel, + selectedCoin, + } = useContext(gameContext); + const [openAlert, setOpenAlert] = React.useState(false); + const [info, setInfo] = React.useState(null); + const handleClickOpen = () => { + setOpen(true); + }; + const handleClose = () => { + setOpen(false); + setForeignAmount(0); + setQortAmount(0); + }; - const createSellOrder = async() => { - try { - setOpen(true) - setInfo({ - type: 'info', - message: "Attempting to create sell order. Please wait..." - }) - const res = await qortalRequestWithTimeout({ - action: "CREATE_TRADE_SELL_ORDER", - qortAmount, - foreignBlockchain: 'LITECOIN', - foreignAmount - }, 900000); - - if(res?.error && res?.failedTradeBot){ - await updateTemporaryFailedTradeBots({ - atAddress: res?.failedTradeBot?.atAddress, - status: 'FAILED', - qortAddress: res?.failedTradeBot?.creatorAddress, - - }) - fetchTemporarySellOrders() - setOpenAlert(true) - setInfo({ - type: 'error', - message: "Unable to create sell order. Please try again." - }) - } - if(!res?.error){ - setOpenAlert(true) - setForeignAmount(0) - setQortAmount(0) - setOpen(false) + const createSellOrder = async () => { + try { + setInfo({ + type: "info", + message: "Attempting to create sell order. Please wait...", + }); + const res = await qortalRequestWithTimeout( + { + action: "CREATE_TRADE_SELL_ORDER", + qortAmount, + foreignBlockchain: selectedCoin, + foreignAmount: qortAmount * foreignAmount, + }, + 900000 + ); - setInfo({ - type: 'success', - message: "Sell order created. Please wait a couple of minutes for the network to propogate the changes." - }) - } - } catch (error) { - if(error?.error && error?.failedTradeBot){ - await updateTemporaryFailedTradeBots({ - atAddress: error?.failedTradeBot?.atAddress, - status: 'FAILED', - qortAddress: error?.failedTradeBot?.creatorAddress, - - }) - fetchTemporarySellOrders() - setOpenAlert(true) - setInfo({ - type: 'error', - message: "Unable to create sell order. Please try again." - }) - } - } - } - - const handleCloseAlert = ( - event?: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, - ) => { - if (reason === 'clickaway') { - return; + if (res?.error && res?.failedTradeBot) { + await updateTemporaryFailedTradeBots({ + atAddress: res?.failedTradeBot?.atAddress, + status: "FAILED", + qortAddress: res?.failedTradeBot?.creatorAddress, + }); + fetchTemporarySellOrders(); + setOpenAlert(true); + setInfo({ + type: "error", + message: "Unable to create sell order. Please try again.", + }); } - - setOpenAlert(false); - setInfo(null) - }; - - if(isUsingGateway){ + if (!res?.error) { + setOpenAlert(true); + setForeignAmount(0); + setQortAmount(0); + setOpen(false); - return ( -
- - Managing your sell orders is not possible using a gateway node. Please switch to a local or custom node at the authentication page - -
- ) + setInfo({ + type: "success", + message: + "Sell order created. Please wait a couple of minutes for the network to propogate the changes.", + }); + } + } catch (error) { + if (error?.error && error?.failedTradeBot) { + await updateTemporaryFailedTradeBots({ + atAddress: error?.failedTradeBot?.atAddress, + status: "FAILED", + qortAddress: error?.failedTradeBot?.creatorAddress, + }); + fetchTemporarySellOrders(); + setOpenAlert(true); + setInfo({ + type: "error", + message: "Unable to create sell order. Please try again.", + }); + } } - - return ( -
- - item.status === 'FAILED')} /> + }; - { + if (reason === "clickaway") { + return; + } + + setOpenAlert(false); + setInfo(null); + }; + + if (isUsingGateway) { + return ( +
+ + Managing your sell orders is not possible using a gateway node. Please + switch to a local or custom node at the authentication page + +
+ ); + } + return ( +
+ + item.status === "FAILED")} + /> + + - New Sell Order - QORT for LTC + {`New Sell Order - QORT for ${getCoinLabel()}`} ({ - position: 'absolute', + position: "absolute", right: 8, top: 8, color: theme.palette.grey[500], @@ -202,8 +251,10 @@ export const CreateSell = ({qortAddress, show}) => { - - QORT amount + + + QORT amount + { /> - Price Each (LTC) + {`Price of Each QORT (in ${getCoinLabel()})`} { autoComplete="off" /> - {qortAmount * foreignAmount} LTC for {qortAmount} QORT - Total sell amount needs to be greater than: {minimumAmountSellTrades.LITECOIN.value} {' '} {minimumAmountSellTrades.LITECOIN.ticker} + + {`${qortAmount * foreignAmount} ${getCoinLabel()}`} for{" "} + {qortAmount} QORT + + + Total sell amount needs to be greater than:{" "} + {minimumAmountSellTrades[selectedCoin]?.value}{" "} + {minimumAmountSellTrades[selectedCoin]?.ticker} + - - - + {info?.message}
- ) -} + ); +}; diff --git a/src/components/sell/TradeBotList.tsx b/src/components/sell/TradeBotList.tsx index 8bc86ff..8b52fa1 100644 --- a/src/components/sell/TradeBotList.tsx +++ b/src/components/sell/TradeBotList.tsx @@ -8,9 +8,16 @@ import React, { useRef, useState, } from "react"; -import { autoSizeStrategy } from "../Grids/TradeOffers"; -import { Alert, Box, Snackbar, SnackbarCloseReason, Typography } from "@mui/material"; +import { autoSizeStrategy, baseLocalHost } from "../Grids/TradeOffers"; +import { + Alert, + Box, + Snackbar, + SnackbarCloseReason, + Typography, +} from "@mui/material"; import gameContext from "../../contexts/gameContext"; +import { BuyContainerDivider } from "../Grids/Table-styles"; const defaultColDef = { resizable: true, // Make columns resizable by default @@ -18,58 +25,22 @@ const defaultColDef = { suppressMovable: true, // Prevent columns from being movable }; -const columnDefs: ColDef[] = [ - { - headerCheckboxSelection: false, // Adds a checkbox in the header for selecting all rows - checkboxSelection: true, // Adds checkboxes in each row for selection - headerName: "Select", // You can customize the header name - width: 50, // Adjust the width as needed - pinned: "left", // Optional, to pin this column on the left - resizable: false, - }, - { - headerName: "QORT AMOUNT", - field: "qortAmount", - flex: 1, // Flex makes this column responsive - minWidth: 150, // Ensure it doesn't shrink too much - resizable: true, - }, - { - headerName: "LTC/QORT", - valueGetter: (params) => - +params.data.foreignAmount / +params.data.qortAmount, - sortable: true, - sort: "asc", - flex: 1, // Flex makes this column responsive - minWidth: 150, // Ensure it doesn't shrink too much - resizable: true, - }, - { - headerName: "Total LTC Value", - field: "foreignAmount", - flex: 1, // Flex makes this column responsive - minWidth: 150, // Ensure it doesn't shrink too much - resizable: true, - }, - { - headerName: "Status", - field: "status", - flex: 1, // Flex makes this column responsive - minWidth: 300, // Ensure it doesn't shrink too much - resizable: true, - }, -]; - export default function TradeBotList({ qortAddress, failedTradeBots }) { const [tradeBotList, setTradeBotList] = useState([]); const [selectedTrade, setSelectedTrade] = useState(null); - const tradeBotListRef = useRef([]) + const tradeBotListRef = useRef([]); const offeringTrades = useRef([]); const qortAddressRef = useRef(null); const gridRef = useRef(null); - const {updateTemporaryFailedTradeBots, fetchTemporarySellOrders, deleteTemporarySellOrder} = useContext(gameContext) - const [open, setOpen] = useState(false) - const [info, setInfo] = useState(null) + const { + updateTemporaryFailedTradeBots, + fetchTemporarySellOrders, + deleteTemporarySellOrder, + getCoinLabel, + selectedCoin, + } = useContext(gameContext); + const [open, setOpen] = useState(false); + const [info, setInfo] = useState(null); const filteredOutTradeBotListWithoutFailed = useMemo(() => { const list = tradeBotList.filter( (item) => @@ -77,7 +48,7 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) { (failedItem) => failedItem.atAddress === item.atAddress ) ); - return list + return list; }, [failedTradeBots, tradeBotList]); const onGridReady = useCallback((params: any) => { @@ -88,6 +59,49 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) { params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content }, []); + const columnDefs: ColDef[] = useMemo(() => { + return [ + { + headerCheckboxSelection: false, // Adds a checkbox in the header for selecting all rows + checkboxSelection: true, // Adds checkboxes in each row for selection + headerName: "Select", // You can customize the header name + width: 50, // Adjust the width as needed + pinned: "left", // Optional, to pin this column on the left + resizable: false, + }, + { + headerName: "QORT AMOUNT", + field: "qortAmount", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: `${getCoinLabel()}/QORT`, + valueGetter: (params) => + +params.data.foreignAmount / +params.data.qortAmount, + sortable: true, + sort: "asc", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: `Total ${getCoinLabel()} Value`, + field: "foreignAmount", + flex: 1, // Flex makes this column responsive + minWidth: 150, // Ensure it doesn't shrink too much + resizable: true, + }, + { + headerName: "Status", + field: "status", + flex: 1, // Flex makes this column responsive + minWidth: 300, // Ensure it doesn't shrink too much + resizable: true, + }, + ]; + }, [selectedCoin, getCoinLabel]); useEffect(() => { if (qortAddress) { qortAddressRef.current = qortAddress; @@ -131,7 +145,7 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) { const processTradeBots = (tradeBots) => { let sellTrades = [...tradeBotListRef.current]; // Start with the existing trades - + tradeBots.forEach((trade) => { const status = processTradeBotState(trade); @@ -140,7 +154,7 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) { const existingIndex = sellTrades.findIndex( (existingTrade) => existingTrade.atAddress === trade.atAddress ); - + if (existingIndex > -1) { // Replace the existing trade if it exists sellTrades[existingIndex] = { ...trade, status }; @@ -154,112 +168,159 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) { tradeBotListRef.current = sellTrades; }; + const restartTradeOffers = () => { + if (socketRef.current) { + socketRef.current.close(1000, "forced"); // Close with a custom reason + socketRef.current = null; + } + offeringTrades.current = []; + setTradeBotList([]); + tradeBotListRef.current = []; + }; + + const socketRef = useRef(null); + const initTradeOffersWebSocket = (restarted = false) => { let tradeOffersSocketCounter = 0; let socketTimeout: any; // let socketLink = `ws://127.0.0.1:12391/websockets/crosschain/tradebot?foreignBlockchain=LITECOIN`; - let socketLink = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websockets/crosschain/tradebot?foreignBlockchain=LITECOIN`; - const socket = new WebSocket(socketLink); - socket.onopen = () => { + let socketLink = `${ + window.location.protocol === "https:" ? "wss:" : "ws:" + }//${baseLocalHost}/websockets/crosschain/tradebot?foreignBlockchain=${selectedCoin}`; + socketRef.current = new WebSocket(socketLink); + socketRef.current.onopen = () => { setTimeout(pingSocket, 50); tradeOffersSocketCounter += 1; }; - socket.onmessage = (e) => { + socketRef.current.onmessage = (e) => { tradeOffersSocketCounter += 1; restarted = false; processTradeBots(JSON.parse(e.data)); }; - socket.onclose = () => { + socketRef.current.onclose = (event) => { clearTimeout(socketTimeout); + if (event.reason === "forced") { + return; + } restartTradeOffersWebSocket(); }; - socket.onerror = (e) => { + socketRef.current.onerror = (e) => { clearTimeout(socketTimeout); }; const pingSocket = () => { - socket.send("ping"); + socketRef.current.send("ping"); socketTimeout = setTimeout(pingSocket, 295000); }; }; useEffect(() => { - if(!qortAddress) return - initTradeOffersWebSocket(); - }, [qortAddress]); + if (!qortAddress) return; + if (selectedCoin === null) return; + restartTradeOffers(); + + setTimeout(() => { + initTradeOffersWebSocket(); + }, 500); + return () => { + if (socketRef.current) { + socketRef.current.close(1000, "forced"); + } + }; + }, [qortAddress, selectedCoin]); const onSelectionChanged = (event: any) => { const selectedRows = event.api.getSelectedRows(); - if(selectedRows[0]){ - setSelectedTrade(selectedRows[0]) + if (selectedRows[0]) { + setSelectedTrade(selectedRows[0]); } else { - setSelectedTrade(null) + setSelectedTrade(null); } }; const handleClose = ( event?: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, + reason?: SnackbarCloseReason ) => { - if (reason === 'clickaway') { + if (reason === "clickaway") { return; } - + setOpen(false); - setInfo(null) + setInfo(null); }; - - - const cancelSell = async ()=> { + const cancelSell = async () => { try { - if(!selectedTrade) return - setOpen(true) + if (!selectedTrade) return; + setOpen(true); - setInfo({ - type: 'info', - message: "Attempting to cancel sell order" - }) - const res = await qortalRequestWithTimeout({ - action: "CANCEL_TRADE_SELL_ORDER", - qortAmount: selectedTrade.qortAmount, - foreignBlockchain: 'LITECOIN', - foreignAmount: selectedTrade.foreignAmount, - atAddress: selectedTrade.atAddress - }, 900000); - if(res?.signature){ - await deleteTemporarySellOrder(selectedTrade.atAddress) - - - setSelectedTrade(null) - setOpen(true) - setInfo({ - type: 'success', - message: "Sell order canceled. Please wait a couple of minutes for the network to propogate the changes" - }) - } - if(res?.error && res?.failedTradeBot){ - setOpen(true) - setInfo({ - type: 'error', - message: "Unable to cancel sell order. Please try again." - }) - } - } catch (error) { - if(error?.error && error?.failedTradeBot){ - setOpen(true) setInfo({ - type: 'error', - message: "Unable to cancel sell order. Please try again." - }) - } - } - } + type: "info", + message: "Attempting to cancel sell order", + }); + const res = await qortalRequestWithTimeout( + { + action: "CANCEL_TRADE_SELL_ORDER", + qortAmount: selectedTrade.qortAmount, + foreignBlockchain: selectedTrade.foreignBlockchain, + foreignAmount: selectedTrade.foreignAmount, + atAddress: selectedTrade.atAddress, + }, + 900000 + ); + if (res?.signature) { + await deleteTemporarySellOrder(selectedTrade.atAddress); + + setSelectedTrade(null); + setOpen(true); + setInfo({ + type: "success", + message: + "Sell order canceled. Please wait a couple of minutes for the network to propogate the changes", + }); + } + if (res?.error && res?.failedTradeBot) { + setOpen(true); + setInfo({ + type: "error", + message: "Unable to cancel sell order. Please try again.", + }); + } + } catch (error) { + if (error?.error && error?.failedTradeBot) { + setOpen(true); + setInfo({ + type: "error", + message: "Unable to cancel sell order. Please try again.", + }); + } + } + }; const CancelButton = () => { return ( - ); @@ -298,62 +359,73 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) { )} */}
- - - {/* + + + + {/* {selectedTotalQORT?.toFixed(3)} QORT */} - - {/* + {/* ltcBalance ? 'red' : 'white', + color: selectedTotalLTC > foreignCoinBalance ? 'red' : 'white', }}>{selectedTotalLTC?.toFixed(4)} LTC */} - - - - {/* + {/* {ltcBalance?.toFixed(4)} {foreignCoinBalance?.toFixed(4)} LTC balance */} + + {CancelButton()} - {CancelButton()} - - + {info?.message} diff --git a/src/contexts/gameContext.ts b/src/contexts/gameContext.ts index 487a536..dff3f1b 100644 --- a/src/contexts/gameContext.ts +++ b/src/contexts/gameContext.ts @@ -14,7 +14,7 @@ export interface UserNameAvatar { } export interface IContextProps { - ltcBalance: number | null; + foreignCoinBalance: number | null; qortBalance: number | null; userInfo: any; setUserInfo: (val: any) => void; @@ -32,11 +32,14 @@ export interface IContextProps { updateTemporaryFailedTradeBots: (val: any)=> void; fetchTemporarySellOrders: ()=> void; isUsingGateway: boolean; + selectedCoin: string; + setSelectedCoin: (val: any)=> void; + getCoinLabel: ()=> string | null | void; } const defaultState: IContextProps = { qortBalance: null, - ltcBalance: null, + foreignCoinBalance: null, userInfo: null, setUserInfo: () => {}, userNameAvatar: {}, @@ -52,7 +55,10 @@ const defaultState: IContextProps = { deleteTemporarySellOrder: ()=> {}, updateTemporaryFailedTradeBots: ()=> {}, fetchTemporarySellOrders: ()=> {}, - isUsingGateway: true + isUsingGateway: true, + selectedCoin: 'LITECOIN', + setSelectedCoin: ()=> {}, + getCoinLabel: ()=> {} }; export default React.createContext(defaultState); diff --git a/src/pages/Home/Home-Styles.tsx b/src/pages/Home/Home-Styles.tsx index 7de8ed5..77890c1 100644 --- a/src/pages/Home/Home-Styles.tsx +++ b/src/pages/Home/Home-Styles.tsx @@ -1,64 +1,9 @@ import { Box, Button, Typography } from "@mui/material"; import { styled } from "@mui/system"; -export const BubbleBoard = styled(Box)({ - position: "relative", - display: "grid", - gridTemplateColumns: "repeat(9, 1fr)", - gridTemplateRows: "repeat(4, 1fr)", - gap: "15px", - width: "815px", - height: "353px", -}); - -export const BubbleCard = styled(Box)(({ theme }) => ({ - display: "flex", - alignItems: "center", - justifyContent: "center", - height: "77px", - width: "77px", - background: "#ffffff05", - borderRadius: "50%", - fontFamily: "Fredoka One, sans-serif", - fontWeight: 500, - fontSize: "40px", - lineHeight: "48.4px", - textAlign: "center", - color: theme.palette.text.primary, -})); - - -export const BubbleCardColored1 = styled(Box)({ - height: "77px", - width: "77px", - background: "linear-gradient(124.49deg, #70BAFF 7.03%, #F29999 94.22%)", - boxShadow: "0px 0px 25.8px -1px #1C5A93", - borderRadius: "50%", -}); - -export const BubbleCardColored2 = styled(Box)({ - height: "77px", - width: "77px", - background: "linear-gradient(36.5deg, #70BAFF 19.69%, #F29999 90.73%)", - boxShadow: "0px 0px 25.8px -1px #1C5A93", - borderRadius: "50%", -}); - -export const BubbleCardColored3 = styled(Box)({ - height: "77px", - width: "77px", - background: "linear-gradient(180deg, #70BAFF -24.68%, #ACABD0 25.49%, #F29999 74.03%)", - boxShadow: "0px 0px 25.8px -1px #1C5A93", - borderRadius: "50%", -}); - -export const BubbleCardColored4 = styled(Box)({ - height: "77px", - width: "77px", - background: "linear-gradient(275.71deg, #70BAFF 35.99%, #F29999 95.61%)", - boxShadow: "0px 0px 25.8px -1px #1C5A93", - borderRadius: "50%", -}); +type TabProp = { + activeTab: boolean; +}; export const MainCol = styled(Box)({ display: "flex", @@ -119,4 +64,54 @@ export const HomeWrapper = styled(Box)({ gap: "100px", height: "90vh", width: "100%", -}); \ No newline at end of file +}); + +export const TabsContainer = styled(Box)({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + width: "100%", + justifyContent: "center", +}); + +export const TabsRow = styled(Box)({ + display: "flex", + flexDirection: "row", + gap: "5px", + justifyContent: "space-evenly", + alignItems: "center", + backgroundColor: "#323336", + width: "fit-content", + borderRadius: "5px", + padding: "3px 0", +}); + +export const Tab = styled(Box, { + shouldForwardProp: (prop) => prop !== "activeTab" +})(({ theme, activeTab }) => ({ + color: activeTab ? "#323336" : "#e8e8e8", + fontFamily: "Inter, sans-serif", + fontSize: "16px", + lineHeight: "19.2px", + fontWeight: 400, + backgroundColor: activeTab ? "#e8e8e8" : "transparent", + padding: "5px 10px", + borderRadius: "5px", + height: "auto", + transition: "all 0.4s ease-in-out", + userSelect: "none", + "&:hover": { + color: "#323336", + backgroundColor: "#babbbc", + cursor: activeTab ? "auto" : "pointer", + }, +})); + +export const TabDivider = styled(Box, { + shouldForwardProp: (prop) => prop !== "activeTab" +})(({ theme, activeTab }) => ({ + width: "1px", + height: "25px", + margin: "0 3px", + backgroundColor: activeTab ? "transparent" : "#a4a4a5", +})); \ No newline at end of file diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index e639164..763ae18 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -1,45 +1,41 @@ -import { FC, useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import { AppContainer } from "../../App-styles"; - -import axios from "axios"; import { Header } from "../../components/header/Header"; -import { useLocation, useNavigate } from "react-router-dom"; import gameContext from "../../contexts/gameContext"; -import { Link } from "react-router-dom"; import { NotificationContext } from "../../contexts/notificationContext"; - import { TradeOffers } from "../../components/Grids/TradeOffers"; import { OngoingTrades } from "../../components/Grids/OngoingTrades"; -import { Box, Button, CircularProgress } from "@mui/material"; +import { Box } from "@mui/material"; import { TextTableTitle } from "../../components/Grids/Table-styles"; import { Spacer } from "../../components/common/Spacer"; -import { ReusableModal } from "../../components/common/reusable-modal/ReusableModal"; -import { OAuthButton, OAuthButtonRow } from "./Home-Styles"; +import { Tab, TabDivider, TabsContainer, TabsRow } from "./Home-Styles"; import { CreateSell } from "../../components/sell/CreateSell"; +import { History } from "../../components/history/History"; -interface IsInstalledProps {} - -export const HomePage: FC = ({}) => { - const location = useLocation(); - const navigate = useNavigate(); +export const HomePage = () => { const { qortBalance, - ltcBalance, + foreignCoinBalance, userInfo, - isAuthenticated, setIsAuthenticated, - OAuthLoading, setOAuthLoading, + onGoingTrades, + selectedCoin, } = useContext(gameContext); const { setNotification } = useContext(NotificationContext); const [mode, setMode] = useState("buy"); - + const filteredOngoingTrades = useMemo(() => { + return onGoingTrades?.filter( + (item) => item?.tradeInfo?.foreignBlockchain === selectedCoin + ); + }, [onGoingTrades, selectedCoin]); const checkIfAuthenticated = async () => { try { setOAuthLoading(true); setIsAuthenticated(true); } catch (error) { + console.error(error); } finally { setOAuthLoading(false); } @@ -50,57 +46,74 @@ export const HomePage: FC = ({}) => { }, [userInfo?.address]); return ( - + <>
-
- - + + + setMode("buy")}> + Buy QORT + + {/* */} + setMode("sell")}> + Sell QORT + + {/* */} + {/* setMode("history")} + > + Trade History + */} + + +
- + - My Pending Orders - - - - - - - + {`My Pending Orders: ${filteredOngoingTrades?.length}`} + + + + + + - Open Market Sell Orders - - - - -
+ + Open Market Sell Orders + +
+ + +
- - + + + + ); }; diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index bc26d72..6bc2c01 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -1,6 +1,25 @@ +import moment from "moment"; + export function formatTime(seconds: number): string { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; // Pad the seconds with a leading zero if less than 10 return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } + + +export function formatTimestampForum(timestamp: number): string { + const now = moment(); + const timestampMoment = moment(timestamp); + const elapsedTime = now.diff(timestampMoment, 'minutes'); + + if (elapsedTime < 1) { + return `Just now - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 60) { + return `${elapsedTime}m ago - ${timestampMoment.format('h:mm A')}`; + } else if (elapsedTime < 1440) { + return `${Math.floor(elapsedTime / 60)}h ago - ${timestampMoment.format('h:mm A')}`; + } else { + return timestampMoment.format('MMM D, YYYY - h:mm A'); + } +} \ No newline at end of file