Merge pull request #1 from Qortal/justin/redesign

Justin/redesign
This commit is contained in:
Phillip 2025-01-27 02:20:21 +02:00 committed by GitHub
commit 2017fdee38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2909 additions and 1001 deletions

View File

@ -2,9 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="
connect-src 'self' wss://appnode.qortal.org;
">
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
@ -14,7 +12,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qort.Trade</title> <title>Q-Trade</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

150
package-lock.json generated
View File

@ -19,8 +19,12 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-ga4": "^2.1.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-router-dom": "^6.23.0",
"react-toastify": "^10.0.5", "react-toastify": "^10.0.5",
"sass": "^1.76.0", "sass": "^1.76.0",
@ -3236,6 +3240,11 @@
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true "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": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@ -3826,6 +3835,14 @@
"node": ">=6" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001660", "version": "1.0.30001660",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
@ -3952,6 +3969,14 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true "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": { "node_modules/core-js-compat": {
"version": "3.38.1", "version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz",
@ -4003,6 +4028,24 @@
"node": ">=8" "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": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -5984,7 +6027,6 @@
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -6218,7 +6260,6 @@
"version": "8.4.38", "version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -6242,6 +6283,11 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -6292,6 +6338,11 @@
"node": ">=6" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -6332,6 +6383,26 @@
"node": ">=0.10.0" "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": { "node_modules/react-dom": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "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", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" "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": { "node_modules/react-refresh": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -6788,6 +6887,11 @@
"node": ">= 0.4" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -7041,6 +7145,38 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/stylis": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
@ -7189,6 +7325,11 @@
"node": ">=8.0" "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": { "node_modules/tr46": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
@ -7210,6 +7351,11 @@
"typescript": ">=4.2.0" "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": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -21,8 +21,12 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-ga4": "^2.1.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-router-dom": "^6.23.0",
"react-toastify": "^10.0.5", "react-toastify": "^10.0.5",
"sass": "^1.76.0", "sass": "^1.76.0",

View File

@ -2,13 +2,15 @@ import { Box } from "@mui/material";
import { styled } from "@mui/system"; import { styled } from "@mui/system";
export const AppContainer = styled(Box)(({ theme }) => ({ export const AppContainer = styled(Box)(({ theme }) => ({
width: "100%",
height: "100%", height: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
padding: "1em 0", padding: "20px 30px 0 30px",
paddingBottom: '50px' backgroundColor: "#323336",
[`@media (max-width: 500px)`]: {
padding: "10px 5px 0 5px",
}
})); }));
export const MainContainer = styled(Box)` export const MainContainer = styled(Box)`

View File

@ -125,3 +125,22 @@
font-weight: 600; font-weight: 600;
font-display: swap; 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;
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import "./App.css"; import "./App.css";
import socketService from "./services/socketService"; import socketService from "./services/socketService";
@ -71,7 +71,13 @@ export async function sendRequestToExtension(
function App() { function App() {
const [userInfo, setUserInfo] = useState<any>(null); const [userInfo, setUserInfo] = useState<any>(null);
const [qortBalance, setQortBalance] = useState<any>(null); const [qortBalance, setQortBalance] = useState<any>(null);
const [ltcBalance, setLtcBalance] = useState<any>(null); const [balances, setBalances] = useState<any>({});
const [selectedCoin, setSelectedCoin] = useState("LITECOIN");
const foreignCoinBalance = useMemo(()=> {
if(balances[selectedCoin] === 0) return 0
return balances[selectedCoin] || null
}, [balances, selectedCoin])
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false); const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [OAuthLoading, setOAuthLoading] = useState<boolean>(false); const [OAuthLoading, setOAuthLoading] = useState<boolean>(false);
const db = useIndexedDBContext(); const db = useIndexedDBContext();
@ -172,14 +178,19 @@ function App() {
setQortBalance(balanceResponse.data?.value) setQortBalance(balanceResponse.data?.value)
} }
const getLTCBalance = async () => { const getLTCBalance = async (coin) => {
try { try {
const response = await qortalRequest({ const response = await qortalRequest({
action: "GET_WALLET_BALANCE", action: "GET_WALLET_BALANCE",
coin: "LTC" coin: getCoinLabel(coin)
}); });
if(!response?.error){ if(!response?.error){
setLtcBalance(+response) setBalances((prev)=> {
return {
...prev,
[coin]: +response
}
})
} }
} catch (error) { } catch (error) {
// //
@ -187,18 +198,18 @@ function App() {
} }
useEffect(() => { useEffect(() => {
if(!userInfo?.address) return if(!userInfo?.address || !selectedCoin) return
const intervalGetTradeInfo = setInterval(() => { const intervalGetTradeInfo = setInterval(() => {
fetchOngoingTransactions() fetchOngoingTransactions()
getLTCBalance() getLTCBalance(selectedCoin)
getQortBalance() getQortBalance()
}, 150000) }, 150000)
getLTCBalance() getLTCBalance(selectedCoin)
getQortBalance() getQortBalance()
return () => { return () => {
clearInterval(intervalGetTradeInfo) clearInterval(intervalGetTradeInfo)
} }
}, [userInfo?.address, isAuthenticated]) }, [userInfo?.address, isAuthenticated, selectedCoin])
const handleMessage = async (event: any) => { const handleMessage = async (event: any) => {
@ -208,7 +219,6 @@ function App() {
setAvatar(""); setAvatar("");
setIsAuthenticated(false); setIsAuthenticated(false);
setQortBalance(null) setQortBalance(null)
setLtcBalance(null)
localStorage.setItem("token", ""); localStorage.setItem("token", "");
} else if(event.data.type === "RESPONSE_FOR_TRADES"){ } else if(event.data.type === "RESPONSE_FOR_TRADES"){
@ -246,6 +256,37 @@ function App() {
}; };
}, [userInfo?.address]); }, [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 = { const gameContextValue: IContextProps = {
userInfo, userInfo,
setUserInfo, setUserInfo,
@ -253,7 +294,7 @@ function App() {
setUserNameAvatar, setUserNameAvatar,
onGoingTrades, onGoingTrades,
fetchOngoingTransactions, fetchOngoingTransactions,
ltcBalance, foreignCoinBalance,
qortBalance, qortBalance,
isAuthenticated, isAuthenticated,
setIsAuthenticated, setIsAuthenticated,
@ -261,7 +302,7 @@ function App() {
setOAuthLoading, setOAuthLoading,
updateTransactionInDB, updateTransactionInDB,
sellOrders, sellOrders,
deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, isUsingGateway deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, isUsingGateway, selectedCoin, setSelectedCoin, getCoinLabel
}; };

3
src/assets/SVG/Copy.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.92857 0.5H8.57143C9.36071 0.5 10 1.13929 10 1.92857V6.57143C10 7.36071 9.36071 8 8.57143 8H8.21429V4.42857C8.21429 3.24643 7.25357 2.28571 6.07143 2.28571H2.5V1.92857C2.5 1.13929 3.13929 0.5 3.92857 0.5ZM1.42857 3H6.07143C6.86041 3 7.5 3.63959 7.5 4.42857V9.07143C7.5 9.86041 6.86041 10.5 6.07143 10.5H1.42857C0.639593 10.5 0 9.86041 0 9.07143V4.42857C0 3.63959 0.639593 3 1.42857 3Z" fill="white" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

BIN
src/assets/img/arrr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/img/btc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/img/dgb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/assets/img/doge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/img/ltc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/img/qort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/img/rvn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -5,7 +5,7 @@ import { useIndexedDBContext } from "../../contexts/indexedDBContext";
const fetchTradeInfo = async (qortalAtAddress) => { 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() const data = await checkIfOfferingRes.json()
return data return data
}; };

View File

@ -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 { 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-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css'; import 'ag-grid-community/styles/ag-theme-alpine.css';
import gameContext from '../../contexts/gameContext'; import gameContext from '../../contexts/gameContext';
@ -10,8 +10,18 @@ const autoSizeStrategy: SizeColumnsToContentStrategy = {
}; };
export const OngoingTrades = () => { export const OngoingTrades = () => {
const { onGoingTrades } = useContext(gameContext); const { onGoingTrades, getCoinLabel, selectedCoin } = useContext(gameContext);
const gridRef = useRef<any>(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 = { const defaultColDef = {
resizable: true, // Make columns resizable by default resizable: true, // Make columns resizable by default
@ -35,9 +45,10 @@ export const OngoingTrades = () => {
resizable: true , resizable: true ,
flex: 1, minWidth: 100 flex: 1, minWidth: 100
}, },
{ headerName: "Amount (QORT)", valueGetter: (params) => +params.data.tradeInfo.qortAmount, resizable: true, flex: 1, minWidth: 100 }, { headerName: "Amount (QORT)", valueGetter: (params) => +params.data.tradeInfo.qortAmount, resizable: true, flex: 1, minWidth: 150 },
{ headerName: "LTC/QORT", valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount / +params.data.tradeInfo.qortAmount , resizable: true , flex: 1, minWidth: 100}, { headerName: `${getCoinLabel()}/QORT`, valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount / +params.data.tradeInfo.qortAmount , resizable: true , flex: 1, minWidth: 150},
{ headerName: "Total LTC Value", valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount, resizable: true , flex: 1, minWidth: 100 }, { headerName: `Total ${getCoinLabel()} Value`, valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount, resizable: true , flex: 1, minWidth: 150,
},
{ {
headerName: "Notes", valueGetter: (params) => { headerName: "Notes", valueGetter: (params) => {
if (params.data.tradeInfo.mode === 'TRADING') { if (params.data.tradeInfo.mode === 'TRADING') {
@ -54,7 +65,7 @@ export const OngoingTrades = () => {
} }
if (params.data.message) return params.data.message 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; // return null;
// }; // };
const getRowId = useCallback(function (params: any) { const getRowId = useCallback(function (params: any) {
return String(params.data._id); return String(params.data?.qortalAtAddress);
}, []); }, []);
return ( return (
<div className="ag-theme-alpine-dark" style={{ height: 225, width: '100%' }}> <div className="ag-theme-alpine-dark" style={{ height: 225, width: '100%', display: filteredOngoingTrades?.length === 0 && 'none' }}>
<AgGridReact <AgGridReact
onGridReady={onGridReady}
ref={gridRef}
columnDefs={columnDefs} columnDefs={columnDefs}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
rowData={onGoingTrades} rowData={filteredOngoingTrades}
// onRowClicked={onRowClicked} // onRowClicked={onRowClicked}
rowSelection="single" rowSelection="single"
getRowId={getRowId} getRowId={getRowId}

View File

@ -1,5 +1,12 @@
import { styled } from "@mui/system"; import { Box, styled } from "@mui/system";
import { Box, Typography } from "@mui/material"; 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 }) => ({ export const TextTableTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Inter", fontFamily: "Inter",
@ -8,4 +15,48 @@ export const TextTableTitle = styled(Typography)(({ theme }) => ({
fontSize: "20px", fontSize: "20px",
lineHeight: "40px", lineHeight: "40px",
userSelect: "none", 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",
}
}));

File diff suppressed because it is too large Load Diff

View File

@ -55,22 +55,22 @@ export const Terms =() => {
</IconButton> </IconButton>
<DialogContent dividers> <DialogContent dividers>
<Typography gutterBottom> <Typography gutterBottom>
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 usethere 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 usethere are no additional fees for buying QORT through this site. There are two ways to place a buy order:
1. Use the gateway 1. Use the gateway
2. Use your local node. 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.
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
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.
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
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.
</Typography> </Typography>
<Typography gutterBottom> <Typography gutterBottom>
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.
</Typography> </Typography>
</DialogContent> </DialogContent>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,5 +1,6 @@
import { Box, Button } from "@mui/material"; import { Box, Button } from "@mui/material";
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import CloseIcon from '@mui/icons-material/Close';
export const ReusableModalContainer = styled(Box)(({ theme }) => ({ export const ReusableModalContainer = styled(Box)(({ theme }) => ({
display: "flex", 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)", "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)({ export const ReusableModalBackdrop = styled(Box)({
position: "fixed", position: "fixed",
top: "0", top: "0",
@ -51,3 +43,16 @@ export const ReusableModalButton = styled(Button)(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
boxShadow: "1px 4px 10.5px 0px #0000004D" 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)",
},
}));

View File

@ -1,20 +1,26 @@
import { import {
ReusableModalBackdrop, ReusableModalBackdrop,
ReusableModalCloseIcon,
ReusableModalContainer, ReusableModalContainer,
ReusableModalSubContainer,
} from "./ReusableModal-styles"; } from "./ReusableModal-styles";
interface ReusableModalProps { interface ReusableModalProps {
backdrop?: boolean; backdrop?: boolean;
onClickClose: () => void;
children: React.ReactNode; children: React.ReactNode;
} }
export const ReusableModal: React.FC<ReusableModalProps> = ({ backdrop, children }) => { export const ReusableModal: React.FC<ReusableModalProps> = ({
backdrop,
children,
onClickClose,
}) => {
return ( return (
<> <>
<ReusableModalContainer> <ReusableModalContainer>
<ReusableModalSubContainer>{children}</ReusableModalSubContainer> <ReusableModalCloseIcon onClick={onClickClose} />
{children}
</ReusableModalContainer> </ReusableModalContainer>
{backdrop && <ReusableModalBackdrop />} {backdrop && <ReusableModalBackdrop onClick={onClickClose} />}
</> </>
); );
}; };

View File

@ -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 (
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
flexDirection: "column",
marginTop: '10px'
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
flexDirection: "column",
marginTop: "20px",
}}
>
<QRCode
value={targetAddress} // Your address here
size={150} // Adjust size as needed
level="M" // Error correction level (L, M, Q, H)
bgColor="#FFFFFF" // Background color (white)
fgColor="#000000" // Foreground color (black)
/>
</Box>
</Box>
</Box>
);
};

View File

@ -1,5 +1,5 @@
import { styled } from "@mui/system"; 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 { HomeSVG } from "../common/icons/HomeSVG";
import { QortalLogoSVG } from "../common/icons/QortalLogoSVG"; import { QortalLogoSVG } from "../common/icons/QortalLogoSVG";
import { CaretDownSVG } from "../common/icons/CaretDownSVG"; 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)({ export const HomeIcon = styled(HomeSVG)({
cursor: "pointer", cursor: "pointer",
}); });
@ -110,7 +118,7 @@ export const GameSelectDropdownMenuItem = styled(Box)(({ theme }) => ({
export const Username = styled(Typography)(({ theme }) => ({ export const Username = styled(Typography)(({ theme }) => ({
fontFamily: "Fira Sans, sans-serif", fontFamily: "Fira Sans, sans-serif",
fontSize: "16px", fontSize: "20px",
lineHeight: "19.2px", lineHeight: "19.2px",
fontWeight: 400, fontWeight: 400,
color: theme.palette.text.primary, color: theme.palette.text.primary,
@ -130,13 +138,14 @@ export const LogoColumn = styled(Box)({
gap: "10px", gap: "10px",
alignItems: "center", alignItems: "center",
}); });
export const RightColumn = styled(Box)({ export const RightColumn = styled(Box)({
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
gap: "10px", gap: "10px",
alignItems: "flex-start", alignItems: "flex-start",
padding: '10px'
}); });
export const AvatarCircle = styled("img")({ export const AvatarCircle = styled("img")({
borderRadius: "50%", borderRadius: "50%",
width: "35px", width: "35px",
@ -145,12 +154,188 @@ export const AvatarCircle = styled("img")({
userSelect: "none", userSelect: "none",
}); });
export const HeaderText = styled(Typography)(({ theme }) => ({ export const HeaderText = styled(Typography)(({ theme }) => ({
fontFamily: "Inter", fontFamily: "Inter",
color: theme.palette.text.primary, color: theme.palette.text.primary,
textAlign: "center",
fontWeight: 500, fontWeight: 500,
fontSize: "16px", fontSize: "16px",
lineHeight: 1.2, lineHeight: 1.2,
userSelect: "none", 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,
},
}));

View File

@ -1,41 +1,63 @@
import { useState, useEffect, useRef, useContext, ChangeEvent } from "react"; import { useState, useEffect, useRef, useContext, ChangeEvent, useMemo } from "react";
import ReactGA from "react-ga4";
import { import {
AvatarCircle, BubbleCardColored1,
CaretDownIcon, CoinActionContainer,
DropdownContainer, CoinActionRow,
GameSelectDropdown, CoinActionsRow,
GameSelectDropdownMenu, CoinCancelBtn,
GameSelectDropdownMenuItem, CoinConfirmSendBtn,
CoinReceiveBtn,
CoinSelectRow,
CoinSendBtn,
CustomInputField,
HeaderNav, HeaderNav,
HeaderRow,
HeaderText, HeaderText,
HomeIcon,
LogoColumn, LogoColumn,
NameRow, NameRow,
QortalLogoIcon,
RightColumn, RightColumn,
SendFont,
TotalCol,
Username, Username,
} from "./Header-styles"; } from "./Header-styles";
import gameContext from "../../contexts/gameContext"; import gameContext from "../../contexts/gameContext";
import { UserContext } from "../../contexts/userContext"; import { UserContext } from "../../contexts/userContext";
import { cropAddress } from "../../utils/cropAddress"; import { cropAddress } from "../../utils/cropAddress";
import { BubbleCardColored1 } from "../../pages/Home/Home-Styles"; import qtradeLogo from "../../components/common/icons/qtradeLogo.png";
import logoSVG from "../../assets/SVG/LOGO.svg"; 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 { import {
Alert, Alert,
AppBar,
Avatar, Avatar,
Box, Box,
Button, Card,
CardContent,
FormControl,
FormControlLabel, FormControlLabel,
MenuItem, MenuItem,
Select, Select,
Snackbar, Snackbar,
SnackbarCloseReason, SnackbarCloseReason,
Switch, Switch,
Typography,
styled, styled,
} from "@mui/material"; } from "@mui/material";
import { sendRequestToExtension } from "../../App"; import { sendRequestToExtension } from "../../App";
import { Terms } from "../Terms"; 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 () => { const checkIfLocal = async () => {
try { 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 (
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
height: "25px",
}}
>
<img
style={{
height: "20px",
width: "auto",
}}
src={img}
/>
<p>{coin}</p>
</div>
);
};
export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
const [openDropdown, setOpenDropdown] = useState<boolean>(false); const [openDropdown, setOpenDropdown] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLDivElement>(null); const buttonRef = useRef<HTMLDivElement>(null);
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
const [openCoinActionModal, setOpenCoinActionModal] =
useState<CoinModalProps | null>(null);
const [receiverAddress, setReceiverAddress] = useState<string>("");
const [senderAddress, setSenderAddress] = useState<string>("");
const [amount, setAmount] = useState<string>("");
const [coinAddresses, setCoinAddresses] = useState({});
const { isUsingGateway } = useContext(gameContext); const { isUsingGateway } = useContext(gameContext);
const [selectedCoin, setSelectedCoin] = useState("LITECOIN");
const handleChange = (event: ChangeEvent<HTMLInputElement>) => { const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(false); 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", message: "Change the node you are using at the authentication page",
}); });
}; };
const { userInfo } = useContext(gameContext); const { userInfo, selectedCoin, setSelectedCoin, getCoinLabel } =
const { avatar, setAvatar } = useContext(UserContext); useContext(gameContext);
const { setNotification } = useContext(NotificationContext);
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
padding: 8, padding: 8,
@ -151,110 +239,263 @@ export const Header = ({ qortBalance, ltcBalance, mode, setMode }: any) => {
// } // }
// }, [userInfo]); // }, [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 ( return (
<>
<AppBar
position="sticky"
sx={{
background: "rgba(39, 40, 44, 1)",
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
padding: "10px",
}}
>
<LogoColumn>
<img
src={qtradeLogo}
style={{
height: "40px",
}}
/>
</LogoColumn>
<FormControlLabel
sx={{
color: "white",
}}
control={
<LocalNodeSwitch
checked={isUsingGateway}
onChange={handleChange}
/>
}
label="Is using Gateway"
/>
</Box>
</AppBar>
<HeaderNav <HeaderNav
sx={{ sx={{
flexDirection: "column", flexDirection: "column",
gap: "10px", gap: "10px",
}} }}
> >
<Spacer height="10px" />
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "20px", gap: "20px",
alignItems: "center", alignItems: "center",
width: "100%",
justifyContent: "space-between",
}} }}
> >
<LogoColumn>
<img
src={logoSVG}
style={{
height: "24px",
}}
/>
</LogoColumn>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Select
size="small"
value={selectedCoin}
onChange={(e) => setSelectedCoin(e.target.value)}
>
<MenuItem value={"LITECOIN"}>LTC</MenuItem>
</Select>
</Box>
</Box>
<RightColumn>
<HeaderText>
Balance: {qortBalance} QORT |{" "}
{ltcBalance === null ? "N/A" : ltcBalance} LTC
</HeaderText>
<NameRow> <NameRow>
{userInfo?.name ? (
<Username>{userInfo?.name}</Username>
) : userInfo?.address ? (
<Username>{cropAddress(userInfo?.address)}</Username>
) : null}
{userInfo?.name ? ( {userInfo?.name ? (
<Avatar <Avatar
sx={{ sx={{
height: "24px", height: "30px",
width: "24px", width: "30px",
fontSize: "15px", fontSize: "16px",
}} }}
src={`/arbitrary/THUMBNAIL/${userInfo?.name}/qortal_avatar?encoding=base64&rebuild=false`} src={`/arbitrary/THUMBNAIL/${userInfo?.name}/qortal_avatar?async=true`}
alt={`${userInfo?.name}`} alt={`${userInfo?.name}`}
> >
{userInfo?.name?.charAt(0)?.toUpperCase()} {userInfo?.name?.charAt(0)?.toUpperCase()}
</Avatar> </Avatar>
) : userInfo?.address ? ( ) : userInfo?.address ? (
<BubbleCardColored1 style={{ height: "35px", width: "35px" }} /> <BubbleCardColored1 style={{ height: "35px", width: "35px" }} />
) : ( ) : null}
<QortalLogoIcon {userInfo?.name ? (
height="35" <Username>{userInfo?.name}</Username>
width="35" ) : userInfo?.address ? (
color="none" <Username>{cropAddress(userInfo?.address)}</Username>
onClickFunc={() => { ) : null}
window.open("https://www.qortal.dev", "_blank")?.focus();
}}
/>
)}
</NameRow> </NameRow>
</RightColumn>
<FormControlLabel
sx={{
color: "white",
}}
control={
<LocalNodeSwitch checked={isUsingGateway} onChange={handleChange} />
}
label="Is using Gateway"
/>
<Terms /> <Terms />
<Box </Box>
<RightColumn
sx={{ sx={{
display: "flex",
gap: "20px",
alignItems: "center", alignItems: "center",
}} }}
> >
<Button onClick={() => setMode("buy")}>Buy QORT</Button> <Card
<Button onClick={() => setMode("sell")}>SELL QORT</Button> variant="outlined"
sx={{
backgroundColor: "#292929",
"&.MuiCard-root": {
cursor: "default",
},
}}
>
<CardContent>
<HeaderText>Total Balance</HeaderText>
<Spacer height="10px" />
<TotalCol>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
<img
src={qortIcon}
style={{
height: "25px",
width: "auto",
}}
/>
<HeaderText>{qortBalance} QORT</HeaderText>
</Box> </Box>
<CoinActionsRow>
<CoinSendBtn
onClick={() => {
setOpenCoinActionModal({
coin: "QORT",
type: "send",
});
}}
>
Send
</CoinSendBtn>
<CoinReceiveBtn
onClick={() => {
setOpenCoinActionModal({
coin: "QORT",
type: "receive",
});
}}
>
Receive
</CoinReceiveBtn>
</CoinActionsRow>
</TotalCol>
<Spacer height="10px" />
<TotalCol>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
<img
src={getCoinIcon(getCoinLabel())}
style={{
height: "25px",
width: "auto",
}}
/>
{foreignCoinBalance === null ? (
<FallingLines
color="white"
width="30"
visible={true}
/>
) : foreignCoinBalance}{" "}
{getCoinLabel()}
</Box>
<CoinActionsRow>
<CoinSendBtn
onClick={() => {
setOpenCoinActionModal({
coin: selectedCoin,
type: "send",
});
}}
>
Send
</CoinSendBtn>
<CoinReceiveBtn
onClick={() => {
setOpenCoinActionModal({
coin: selectedCoin,
type: "receive",
});
}}
>
Receive
</CoinReceiveBtn>
</CoinActionsRow>
</TotalCol>
</CardContent>
</Card>
</RightColumn>
<CoinSelectRow>
<Select
size="small"
value={selectedCoin}
onChange={(e) => setSelectedCoin(e.target.value)}
>
<MenuItem value={"LITECOIN"}>
<SelectRow coin="LTC" />
</MenuItem>
<MenuItem value={"DOGECOIN"}>
<SelectRow coin="DOGE" />
</MenuItem>
<MenuItem value={"BITCOIN"}>
<SelectRow coin="BTC" />
</MenuItem>
<MenuItem value={"DIGIBYTE"}>
<SelectRow coin="DGB" />
</MenuItem>
<MenuItem value={"RAVENCOIN"}>
<SelectRow coin="RVN" />
</MenuItem>
<MenuItem value={"PIRATECHAIN"}>
<SelectRow coin="ARRR" />
</MenuItem>
</Select>
</CoinSelectRow>
<Snackbar <Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }} anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={open} open={open}
autoHideDuration={6000} autoHideDuration={info?.autoHideDurationOff ? null : 6000}
onClose={handleClose} onClose={handleClose}
> >
{info?.type && (
<Alert <Alert
onClose={handleClose} onClose={handleClose}
severity={info?.type} severity={info?.type}
@ -263,7 +504,245 @@ export const Header = ({ qortBalance, ltcBalance, mode, setMode }: any) => {
> >
{info?.message} {info?.message}
</Alert> </Alert>
)}
</Snackbar> </Snackbar>
{openCoinActionModal && (
<ReusableModal
onClickClose={() => {
setOpenCoinActionModal(null);
setAmount('')
setSenderAddress('')
}}
backdrop
>
<CoinActionContainer>
{openCoinActionModal.type === "send" ? <>
<CoinActionRow>
<HeaderRow>
{openCoinActionModal.type === "send" &&
openCoinActionModal.coin === "QORT" ? (
<>
<SendFont>Send {openCoinActionModal.coin}</SendFont>
<img
src={qortIcon}
style={{
height: "25px",
width: "auto",
}}
/>
</>
) : openCoinActionModal.type === "send" &&
openCoinActionModal.coin !== "QORT" ? (
<>
<SendFont>Send {openCoinActionModal.coin}</SendFont>
<img
src={getCoinIcon(getCoinLabel())}
style={{
height: "25px",
width: "auto",
}}
/>
</>
) : null}
</HeaderRow>
</CoinActionRow>
<CoinActionRow>
<FormControl fullWidth>
<CustomInputField
style={{ flexGrow: 1 }}
name={
openCoinActionModal.type === "send"
? `${openCoinActionModal.coin === "QORT" ? 'Recipient Address or Name' : 'Recipient Address'}`
: "Receive Address"
}
label={
openCoinActionModal.type === "send"
? `${openCoinActionModal.coin === "QORT" ? 'Recipient Address or Name' : 'Recipient Address'}`
: "Receive Address"
}
variant="filled"
value={
openCoinActionModal.type === "send"
? senderAddress
: receiverAddress
}
required
onChange={(e) => {
if (openCoinActionModal.type === "send") {
setSenderAddress(e.target.value);
} else {
setReceiverAddress(e.target.value);
}
}}
/>
</FormControl>
</CoinActionRow>
{openCoinActionModal.type === "send" && (
<CoinActionRow>
<FormControl fullWidth>
<CustomInputField
style={{ flexGrow: 1 }}
name="Amount"
label="Amount"
variant="filled"
type="number"
value={
amount
}
required
onChange={(e) => {
setAmount(e.target.value)
}}
/>
</FormControl>
</CoinActionRow>
)}
</> : (
<>
<ReceiveCoin setOpen={setOpen} setInfo={setInfo} coinAddresses={coinAddresses} setCoinAddresses={setCoinAddresses} selectedCoin={openCoinActionModal.coin === "QORT" ? 'QORT' :getCoinLabel()} />
</>
)}
{openCoinActionModal.type === 'send' && (
<CoinActionRow style={{gap: "10px"}}>
{/* <CoinCancelBtn onClick={() => setOpenCoinActionModal(null)}>
Cancel
</CoinCancelBtn> */}
<CoinConfirmSendBtn
onClick={() => {
if(openCoinActionModal.type === 'send'){
sendCoin()
}
setNotification({
alertType: "alertInfo",
msg: "Sending...",
});
}}
>
{openCoinActionModal.type === "send" ? "Send" : "Receive"}
</CoinConfirmSendBtn>
</CoinActionRow>
)}
</CoinActionContainer>
</ReusableModal>
)}
</HeaderNav> </HeaderNav>
</>
); );
}; };
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 (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<Typography sx={{
color: 'white'
}}>{`Send ${selectedCoin} to your address below`}</Typography>
<Spacer height="20px" />
{foreignAddress && (
<CopyToClipboard text={foreignAddress} onCopy={()=> {
setOpen(true);
setInfo({
type: "info",
message: "Address copied!",
});
}}>
<AddressBox>
{foreignAddress} <img src={Copy} />
</AddressBox>
</CopyToClipboard>
)}
{foreignAddress && (
<>
<AddressQRCode targetAddress={foreignAddress} />
</>
)}
{errorMsg && (
<>
<Spacer height="20px" />
<Typography sx={{
color: 'white'
}}>{errorMsg}</Typography>
</>
)}
</Box>
)
}

View File

@ -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",
})<HistoryBtnProp>(({ 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",
}));

View File

@ -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 (
<Box
style={{
width: "100%",
display: show ? "block" : "none",
}}
>
<HistoryButtonRow>
<HistoryButton
activeBtn={mode === "buyHistory"}
onClick={() => {
setMode("buyHistory");
}}
>
Buy History
</HistoryButton>
<HistoryButton
activeBtn={mode === "sellHistory"}
onClick={() => {
setMode("sellHistory");
}}
>
Sell History
</HistoryButton>
<ButtonBase
onClick={() => {
getBuyHistory(qortAddress, selectedCoin, mode);
}}
>
<Refresh />
</ButtonBase>
</HistoryButtonRow>
<ShowingFont>Showing most recent 20 results</ShowingFont>
<HistoryList qortAddress={qortAddress} historyList={selectedHistory} />
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={open}
onClose={() => {
setOpen(false);
}}
>
<Alert
onClose={() => setOpen(false)}
severity="info"
variant="filled"
sx={{ width: "100%" }}
>
{"Fetching History"}
</Alert>
</Snackbar>
</Box>
);
};

View File

@ -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<any>(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 (
<div
style={{
width: "100%",
}}
>
<div
className="ag-theme-alpine-dark"
style={{ height: 400, width: "100%" }}
>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowData={historyList}
// onRowClicked={onRowClicked}
// onSelectionChanged={onSelectionChanged}
// getRowStyle={getRowStyle}
autoSizeStrategy={autoSizeStrategy}
rowSelection="single" // Enable multi-select
suppressHorizontalScroll={false} // Allow horizontal scroll on mobile if needed
suppressCellFocus={true} // Prevents cells from stealing focus in mobile
// pagination={true}
// paginationPageSize={10}
onGridReady={onGridReady}
// domLayout='autoHeight'
// getRowId={(params) => params.data.qortalAtAddress} // Ensure rows have unique IDs
/>
</div>
</div>
);
}

View File

@ -1,10 +1,24 @@
import { Alert, Box, Button, DialogActions, DialogContent, DialogTitle, IconButton, InputLabel, Snackbar, SnackbarCloseReason, TextField, Typography, styled } from '@mui/material' import {
import React, { useContext } from 'react' Alert,
import { BootstrapDialog } from '../Terms' Box,
import CloseIcon from '@mui/icons-material/Close'; Button,
import { Spacer } from '../common/Spacer'; DialogActions,
import gameContext from '../../contexts/gameContext'; DialogContent,
import TradeBotList from './TradeBotList'; 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)` export const CustomLabel = styled(InputLabel)`
font-weight: 400; font-weight: 400;
@ -12,15 +26,34 @@ export const CustomLabel = styled(InputLabel)`
font-size: 10px; font-size: 10px;
line-height: 12px; line-height: 12px;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
`;
`
export const minimumAmountSellTrades = { export const minimumAmountSellTrades = {
'LITECOIN': { LITECOIN: {
value: 0.01, 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)({ export const CustomInput = styled(TextField)({
width: "183px", // Adjust the width as needed width: "183px", // Adjust the width as needed
@ -41,13 +74,13 @@ export const CustomInput = styled(TextField)({
}, },
"& .MuiOutlinedInput-root": { "& .MuiOutlinedInput-root": {
"& fieldset": { "& fieldset": {
border: '0.5px solid rgba(255, 255, 255, 0.5)', border: "0.5px solid rgba(255, 255, 255, 0.5)",
}, },
"&:hover fieldset": { "&:hover fieldset": {
border: '0.5px solid rgba(255, 255, 255, 0.5)', border: "0.5px solid rgba(255, 255, 255, 0.5)",
}, },
"&.Mui-focused fieldset": { "&.Mui-focused fieldset": {
border: '0.5px solid rgba(255, 255, 255, 0.5)', border: "0.5px solid rgba(255, 255, 255, 0.5)",
}, },
}, },
"& .MuiInput-underline:before": { "& .MuiInput-underline:before": {
@ -59,141 +92,157 @@ export const CustomInput = styled(TextField)({
"& .MuiInput-underline:after": { "& .MuiInput-underline:after": {
borderBottom: "none", borderBottom: "none",
}, },
}); });
export const CreateSell = ({ qortAddress, show }) => {
export const CreateSell = ({qortAddress, show}) => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [qortAmount, setQortAmount] = React.useState(0) const [qortAmount, setQortAmount] = React.useState(0);
const [foreignAmount, setForeignAmount] = React.useState(0) const [foreignAmount, setForeignAmount] = React.useState(0);
const {updateTemporaryFailedTradeBots, sellOrders, fetchTemporarySellOrders, isUsingGateway} = useContext(gameContext) const {
const [openAlert, setOpenAlert] = React.useState(false) updateTemporaryFailedTradeBots,
const [info, setInfo] = React.useState<any>(null) sellOrders,
fetchTemporarySellOrders,
isUsingGateway,
getCoinLabel,
selectedCoin,
} = useContext(gameContext);
const [openAlert, setOpenAlert] = React.useState(false);
const [info, setInfo] = React.useState<any>(null);
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true); setOpen(true);
}; };
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
setForeignAmount(0) setForeignAmount(0);
setQortAmount(0) setQortAmount(0);
}; };
const createSellOrder = async() => { const createSellOrder = async () => {
try { try {
setOpen(true)
setInfo({ setInfo({
type: 'info', type: "info",
message: "Attempting to create sell order. Please wait..." message: "Attempting to create sell order. Please wait...",
}) });
const res = await qortalRequestWithTimeout({ const res = await qortalRequestWithTimeout(
{
action: "CREATE_TRADE_SELL_ORDER", action: "CREATE_TRADE_SELL_ORDER",
qortAmount, qortAmount,
foreignBlockchain: 'LITECOIN', foreignBlockchain: selectedCoin,
foreignAmount foreignAmount: qortAmount * foreignAmount,
}, 900000); },
900000
);
if(res?.error && res?.failedTradeBot){ if (res?.error && res?.failedTradeBot) {
await updateTemporaryFailedTradeBots({ await updateTemporaryFailedTradeBots({
atAddress: res?.failedTradeBot?.atAddress, atAddress: res?.failedTradeBot?.atAddress,
status: 'FAILED', status: "FAILED",
qortAddress: res?.failedTradeBot?.creatorAddress, qortAddress: res?.failedTradeBot?.creatorAddress,
});
}) fetchTemporarySellOrders();
fetchTemporarySellOrders() setOpenAlert(true);
setOpenAlert(true)
setInfo({ setInfo({
type: 'error', type: "error",
message: "Unable to create sell order. Please try again." message: "Unable to create sell order. Please try again.",
}) });
} }
if(!res?.error){ if (!res?.error) {
setOpenAlert(true) setOpenAlert(true);
setForeignAmount(0) setForeignAmount(0);
setQortAmount(0) setQortAmount(0);
setOpen(false) setOpen(false);
setInfo({ setInfo({
type: 'success', type: "success",
message: "Sell order created. Please wait a couple of minutes for the network to propogate the changes." message:
}) "Sell order created. Please wait a couple of minutes for the network to propogate the changes.",
});
} }
} catch (error) { } catch (error) {
if(error?.error && error?.failedTradeBot){ if (error?.error && error?.failedTradeBot) {
await updateTemporaryFailedTradeBots({ await updateTemporaryFailedTradeBots({
atAddress: error?.failedTradeBot?.atAddress, atAddress: error?.failedTradeBot?.atAddress,
status: 'FAILED', status: "FAILED",
qortAddress: error?.failedTradeBot?.creatorAddress, qortAddress: error?.failedTradeBot?.creatorAddress,
});
}) fetchTemporarySellOrders();
fetchTemporarySellOrders() setOpenAlert(true);
setOpenAlert(true)
setInfo({ setInfo({
type: 'error', type: "error",
message: "Unable to create sell order. Please try again." message: "Unable to create sell order. Please try again.",
}) });
}
} }
} }
};
const handleCloseAlert = ( const handleCloseAlert = (
event?: React.SyntheticEvent | Event, event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason, reason?: SnackbarCloseReason
) => { ) => {
if (reason === 'clickaway') { if (reason === "clickaway") {
return; return;
} }
setOpenAlert(false); setOpenAlert(false);
setInfo(null) setInfo(null);
}; };
if(isUsingGateway){ if (isUsingGateway) {
return ( return (
<div style={{ <div
width: '100%', style={{
display: show ? 'flex' : 'none', width: "100%",
height: '500px', display: show ? "flex" : "none",
alignItems: 'center', height: "500px",
justifyContent: 'center', alignItems: "flex-start",
}}> marginTop: "20px",
<Typography sx={{ justifyContent: "center",
color: 'white', }}
maxWidth: '340px', >
padding: '10px' <Typography
}}> sx={{
Managing your sell orders is not possible using a gateway node. Please switch to a local or custom node at the authentication page color: "white",
maxWidth: "340px",
padding: "10px",
}}
>
Managing your sell orders is not possible using a gateway node. Please
switch to a local or custom node at the authentication page
</Typography> </Typography>
</div> </div>
) );
} }
return ( return (
<div style={{ <div
width: '100%', style={{
display: show ? 'block' : 'none' width: "100%",
}}> display: show ? "block" : "none",
}}
>
<Button onClick={handleClickOpen}>New Sell Order</Button> <Button onClick={handleClickOpen}>New Sell Order</Button>
<TradeBotList qortAddress={qortAddress} failedTradeBots={sellOrders.filter((item)=> item.status === 'FAILED')} /> <TradeBotList
qortAddress={qortAddress}
failedTradeBots={sellOrders.filter((item) => item.status === "FAILED")}
/>
<BootstrapDialog <BootstrapDialog
onClose={handleClose} onClose={handleClose}
aria-labelledby="customized-dialog-title" aria-labelledby="customized-dialog-title"
open={open} open={open}
sx={{ sx={{
'& .MuiDialogContent-root': { "& .MuiDialogContent-root": {
width: '300px' width: "300px",
}, },
}} }}
> >
<DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title"> <DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title">
New Sell Order - QORT for LTC {`New Sell Order - QORT for ${getCoinLabel()}`}
</DialogTitle> </DialogTitle>
<IconButton <IconButton
aria-label="close" aria-label="close"
onClick={handleClose} onClick={handleClose}
sx={(theme) => ({ sx={(theme) => ({
position: 'absolute', position: "absolute",
right: 8, right: 8,
top: 8, top: 8,
color: theme.palette.grey[500], color: theme.palette.grey[500],
@ -203,7 +252,9 @@ export const CreateSell = ({qortAddress, show}) => {
</IconButton> </IconButton>
<DialogContent dividers> <DialogContent dividers>
<Box> <Box>
<CustomLabel htmlFor="standard-adornment-name">QORT amount</CustomLabel> <CustomLabel htmlFor="standard-adornment-name">
QORT amount
</CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<CustomInput <CustomInput
id="standard-adornment-name" id="standard-adornment-name"
@ -214,7 +265,7 @@ export const CreateSell = ({qortAddress, show}) => {
/> />
<Spacer height="6px" /> <Spacer height="6px" />
<CustomLabel htmlFor="standard-adornment-amount"> <CustomLabel htmlFor="standard-adornment-amount">
Price Each (LTC) {`Price of Each QORT (in ${getCoinLabel()})`}
</CustomLabel> </CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<CustomInput <CustomInput
@ -225,34 +276,54 @@ export const CreateSell = ({qortAddress, show}) => {
autoComplete="off" autoComplete="off"
/> />
<Spacer height="6px" /> <Spacer height="6px" />
<Typography>{qortAmount * foreignAmount} LTC for {qortAmount} QORT</Typography> <Typography>
<Typography sx={{ {`${qortAmount * foreignAmount} ${getCoinLabel()}`} for{" "}
fontSize: '12px' {qortAmount} QORT
}}>Total sell amount needs to be greater than: {minimumAmountSellTrades.LITECOIN.value} {' '} {minimumAmountSellTrades.LITECOIN.ticker}</Typography> </Typography>
<Typography
sx={{
fontSize: "14px",
}}
>
Total sell amount needs to be greater than:{" "}
{minimumAmountSellTrades[selectedCoin]?.value}{" "}
{minimumAmountSellTrades[selectedCoin]?.ticker}
</Typography>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={handleClose}> <Button autoFocus onClick={handleClose}>
Close Close
</Button> </Button>
<Button disabled={!qortAmount || !(qortAmount * foreignAmount > minimumAmountSellTrades.LITECOIN.value)} autoFocus onClick={createSellOrder}> <Button
disabled={
!qortAmount ||
!(
qortAmount * foreignAmount >
minimumAmountSellTrades[selectedCoin]?.value
)
}
autoFocus
onClick={createSellOrder}
>
Create sell order Create sell order
</Button> </Button>
</DialogActions> </DialogActions>
</BootstrapDialog> </BootstrapDialog>
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={openAlert} onClose={handleCloseAlert}> <Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={openAlert}
onClose={handleCloseAlert}
>
<Alert <Alert
onClose={handleCloseAlert} onClose={handleCloseAlert}
severity={info?.type} severity={info?.type}
variant="filled" variant="filled"
sx={{ width: '100%' }} sx={{ width: "100%" }}
> >
{info?.message} {info?.message}
</Alert> </Alert>
</Snackbar> </Snackbar>
</div> </div>
) );
} };

View File

@ -8,9 +8,16 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { autoSizeStrategy } from "../Grids/TradeOffers"; import { autoSizeStrategy, baseLocalHost } from "../Grids/TradeOffers";
import { Alert, Box, Snackbar, SnackbarCloseReason, Typography } from "@mui/material"; import {
Alert,
Box,
Snackbar,
SnackbarCloseReason,
Typography,
} from "@mui/material";
import gameContext from "../../contexts/gameContext"; import gameContext from "../../contexts/gameContext";
import { BuyContainerDivider } from "../Grids/Table-styles";
const defaultColDef = { const defaultColDef = {
resizable: true, // Make columns resizable by default resizable: true, // Make columns resizable by default
@ -18,7 +25,42 @@ const defaultColDef = {
suppressMovable: true, // Prevent columns from being movable suppressMovable: true, // Prevent columns from being movable
}; };
const columnDefs: ColDef[] = [ export default function TradeBotList({ qortAddress, failedTradeBots }) {
const [tradeBotList, setTradeBotList] = useState([]);
const [selectedTrade, setSelectedTrade] = useState(null);
const tradeBotListRef = useRef([]);
const offeringTrades = useRef<any[]>([]);
const qortAddressRef = useRef(null);
const gridRef = useRef<any>(null);
const {
updateTemporaryFailedTradeBots,
fetchTemporarySellOrders,
deleteTemporarySellOrder,
getCoinLabel,
selectedCoin,
} = useContext(gameContext);
const [open, setOpen] = useState(false);
const [info, setInfo] = useState<any>(null);
const filteredOutTradeBotListWithoutFailed = useMemo(() => {
const list = tradeBotList.filter(
(item) =>
!failedTradeBots.some(
(failedItem) => failedItem.atAddress === item.atAddress
)
);
return list;
}, [failedTradeBots, tradeBotList]);
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 columnDefs: ColDef[] = useMemo(() => {
return [
{ {
headerCheckboxSelection: false, // Adds a checkbox in the header for selecting all rows headerCheckboxSelection: false, // Adds a checkbox in the header for selecting all rows
checkboxSelection: true, // Adds checkboxes in each row for selection checkboxSelection: true, // Adds checkboxes in each row for selection
@ -35,7 +77,7 @@ const columnDefs: ColDef[] = [
resizable: true, resizable: true,
}, },
{ {
headerName: "LTC/QORT", headerName: `${getCoinLabel()}/QORT`,
valueGetter: (params) => valueGetter: (params) =>
+params.data.foreignAmount / +params.data.qortAmount, +params.data.foreignAmount / +params.data.qortAmount,
sortable: true, sortable: true,
@ -45,7 +87,7 @@ const columnDefs: ColDef[] = [
resizable: true, resizable: true,
}, },
{ {
headerName: "Total LTC Value", headerName: `Total ${getCoinLabel()} Value`,
field: "foreignAmount", field: "foreignAmount",
flex: 1, // Flex makes this column responsive flex: 1, // Flex makes this column responsive
minWidth: 150, // Ensure it doesn't shrink too much minWidth: 150, // Ensure it doesn't shrink too much
@ -58,36 +100,8 @@ const columnDefs: ColDef[] = [
minWidth: 300, // Ensure it doesn't shrink too much minWidth: 300, // Ensure it doesn't shrink too much
resizable: true, resizable: true,
}, },
]; ];
}, [selectedCoin, getCoinLabel]);
export default function TradeBotList({ qortAddress, failedTradeBots }) {
const [tradeBotList, setTradeBotList] = useState([]);
const [selectedTrade, setSelectedTrade] = useState(null);
const tradeBotListRef = useRef([])
const offeringTrades = useRef<any[]>([]);
const qortAddressRef = useRef(null);
const gridRef = useRef<any>(null);
const {updateTemporaryFailedTradeBots, fetchTemporarySellOrders, deleteTemporarySellOrder} = useContext(gameContext)
const [open, setOpen] = useState(false)
const [info, setInfo] = useState<any>(null)
const filteredOutTradeBotListWithoutFailed = useMemo(() => {
const list = tradeBotList.filter(
(item) =>
!failedTradeBots.some(
(failedItem) => failedItem.atAddress === item.atAddress
)
);
return list
}, [failedTradeBots, tradeBotList]);
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
}, []);
useEffect(() => { useEffect(() => {
if (qortAddress) { if (qortAddress) {
qortAddressRef.current = qortAddress; qortAddressRef.current = qortAddress;
@ -154,112 +168,159 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) {
tradeBotListRef.current = sellTrades; 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) => { const initTradeOffersWebSocket = (restarted = false) => {
let tradeOffersSocketCounter = 0; let tradeOffersSocketCounter = 0;
let socketTimeout: any; let socketTimeout: any;
// let socketLink = `ws://127.0.0.1:12391/websockets/crosschain/tradebot?foreignBlockchain=LITECOIN`; // 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`; let socketLink = `${
const socket = new WebSocket(socketLink); window.location.protocol === "https:" ? "wss:" : "ws:"
socket.onopen = () => { }//${baseLocalHost}/websockets/crosschain/tradebot?foreignBlockchain=${selectedCoin}`;
socketRef.current = new WebSocket(socketLink);
socketRef.current.onopen = () => {
setTimeout(pingSocket, 50); setTimeout(pingSocket, 50);
tradeOffersSocketCounter += 1; tradeOffersSocketCounter += 1;
}; };
socket.onmessage = (e) => { socketRef.current.onmessage = (e) => {
tradeOffersSocketCounter += 1; tradeOffersSocketCounter += 1;
restarted = false; restarted = false;
processTradeBots(JSON.parse(e.data)); processTradeBots(JSON.parse(e.data));
}; };
socket.onclose = () => { socketRef.current.onclose = (event) => {
clearTimeout(socketTimeout); clearTimeout(socketTimeout);
if (event.reason === "forced") {
return;
}
restartTradeOffersWebSocket(); restartTradeOffersWebSocket();
}; };
socket.onerror = (e) => { socketRef.current.onerror = (e) => {
clearTimeout(socketTimeout); clearTimeout(socketTimeout);
}; };
const pingSocket = () => { const pingSocket = () => {
socket.send("ping"); socketRef.current.send("ping");
socketTimeout = setTimeout(pingSocket, 295000); socketTimeout = setTimeout(pingSocket, 295000);
}; };
}; };
useEffect(() => { useEffect(() => {
if(!qortAddress) return if (!qortAddress) return;
if (selectedCoin === null) return;
restartTradeOffers();
setTimeout(() => {
initTradeOffersWebSocket(); initTradeOffersWebSocket();
}, [qortAddress]); }, 500);
return () => {
if (socketRef.current) {
socketRef.current.close(1000, "forced");
}
};
}, [qortAddress, selectedCoin]);
const onSelectionChanged = (event: any) => { const onSelectionChanged = (event: any) => {
const selectedRows = event.api.getSelectedRows(); const selectedRows = event.api.getSelectedRows();
if(selectedRows[0]){ if (selectedRows[0]) {
setSelectedTrade(selectedRows[0]) setSelectedTrade(selectedRows[0]);
} else { } else {
setSelectedTrade(null) setSelectedTrade(null);
} }
}; };
const handleClose = ( const handleClose = (
event?: React.SyntheticEvent | Event, event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason, reason?: SnackbarCloseReason
) => { ) => {
if (reason === 'clickaway') { if (reason === "clickaway") {
return; return;
} }
setOpen(false); setOpen(false);
setInfo(null) setInfo(null);
}; };
const cancelSell = async () => {
const cancelSell = async ()=> {
try { try {
if(!selectedTrade) return if (!selectedTrade) return;
setOpen(true) setOpen(true);
setInfo({ setInfo({
type: 'info', type: "info",
message: "Attempting to cancel sell order" message: "Attempting to cancel sell order",
}) });
const res = await qortalRequestWithTimeout({ const res = await qortalRequestWithTimeout(
{
action: "CANCEL_TRADE_SELL_ORDER", action: "CANCEL_TRADE_SELL_ORDER",
qortAmount: selectedTrade.qortAmount, qortAmount: selectedTrade.qortAmount,
foreignBlockchain: 'LITECOIN', foreignBlockchain: selectedTrade.foreignBlockchain,
foreignAmount: selectedTrade.foreignAmount, foreignAmount: selectedTrade.foreignAmount,
atAddress: selectedTrade.atAddress atAddress: selectedTrade.atAddress,
}, 900000); },
if(res?.signature){ 900000
await deleteTemporarySellOrder(selectedTrade.atAddress) );
if (res?.signature) {
await deleteTemporarySellOrder(selectedTrade.atAddress);
setSelectedTrade(null);
setSelectedTrade(null) setOpen(true);
setOpen(true)
setInfo({ setInfo({
type: 'success', type: "success",
message: "Sell order canceled. Please wait a couple of minutes for the network to propogate the changes" message:
}) "Sell order canceled. Please wait a couple of minutes for the network to propogate the changes",
});
} }
if(res?.error && res?.failedTradeBot){ if (res?.error && res?.failedTradeBot) {
setOpen(true) setOpen(true);
setInfo({ setInfo({
type: 'error', type: "error",
message: "Unable to cancel sell order. Please try again." message: "Unable to cancel sell order. Please try again.",
}) });
} }
} catch (error) { } catch (error) {
if(error?.error && error?.failedTradeBot){ if (error?.error && error?.failedTradeBot) {
setOpen(true) setOpen(true);
setInfo({ setInfo({
type: 'error', type: "error",
message: "Unable to cancel sell order. Please try again." message: "Unable to cancel sell order. Please try again.",
}) });
}
} }
} }
};
const CancelButton = () => { const CancelButton = () => {
return ( return (
<button disabled={!selectedTrade || selectedTrade?.status === 'PENDING'} onClick={cancelSell} style={{borderRadius: '8px', width: '150px', height:"30px", background: (!selectedTrade || selectedTrade?.status === 'PENDING') ? 'gray' : "#4D7345", <button
color: 'white', cursor: (!selectedTrade || selectedTrade?.status === 'PENDING') ? 'default' : 'pointer', border: '1px solid #375232', boxShadow: '0px 2.77px 2.21px 0px #00000005' disabled={!selectedTrade || selectedTrade?.status === "PENDING"}
}}> onClick={cancelSell}
style={{
borderRadius: "8px",
width: "150px",
height: "auto",
minHeight: "30px",
background:
!selectedTrade || selectedTrade?.status === "PENDING"
? "gray"
: "#4D7345",
color: "white",
cursor:
!selectedTrade || selectedTrade?.status === "PENDING"
? "default"
: "pointer",
border: "1px solid #375232",
boxShadow: "0px 2.77px 2.21px 0px #00000005",
marginRight: "15px",
}}
>
Cancel sell order Cancel sell order
</button> </button>
); );
@ -298,62 +359,73 @@ export default function TradeBotList({ qortAddress, failedTradeBots }) {
)} */} )} */}
</div> </div>
<Box sx={{ <div
width: '100%', style={{
display: 'flex', height: "120px",
justifyContent: 'space-between', }}
alignItems: 'center', />
position: 'fixed', <Box
bottom: '0px', sx={{
height: '100px', width: "calc(100% - 14px)",
padding: '7px', display: "flex",
background: '#181d1f', justifyContent: "space-between",
alignItems: "center",
}}> position: "fixed",
<Box sx={{ bottom: "0px",
display: 'flex', height: "100px",
gap: '5px', padding: "7px",
flexDirection: 'column', background: "#323336",
width: '100%' }}
}}> >
<BuyContainerDivider />
<Box
sx={{
display: "flex",
gap: "5px",
flexDirection: "column",
width: "100%",
}}
>
{/* <Typography sx={{ {/* <Typography sx={{
fontSize: '16px', fontSize: '16px',
color: 'white', color: 'white',
width: 'calc(100% - 75px)' width: 'calc(100% - 75px)'
}}>{selectedTotalQORT?.toFixed(3)} QORT</Typography> */} }}>{selectedTotalQORT?.toFixed(3)} QORT</Typography> */}
<Box sx={{ <Box
display: 'flex', sx={{
gap: '20px', display: "flex",
alignItems: 'center', gap: "20px",
width: 'calc(100% - 75px)' alignItems: "center",
}}> width: "calc(100% - 75px)",
}}
>
{/* <Typography sx={{ {/* <Typography sx={{
fontSize: '16px', fontSize: '16px',
color: selectedTotalLTC > ltcBalance ? 'red' : 'white', color: selectedTotalLTC > foreignCoinBalance ? 'red' : 'white',
}}><span>{selectedTotalLTC?.toFixed(4)}</span> <span style={{ }}><span>{selectedTotalLTC?.toFixed(4)}</span> <span style={{
marginLeft: 'auto' marginLeft: 'auto'
}}>LTC</span></Typography> */} }}>LTC</span></Typography> */}
</Box> </Box>
{/* <Typography sx={{ {/* <Typography sx={{
fontSize: '16px', fontSize: '16px',
color: 'white', color: 'white',
}}><span>{ltcBalance?.toFixed(4)}</span> <span style={{ }}><span>{foreignCoinBalance?.toFixed(4)}</span> <span style={{
marginLeft: 'auto' marginLeft: 'auto'
}}>LTC balance</span></Typography> */} }}>LTC balance</span></Typography> */}
</Box> </Box>
{CancelButton()} {CancelButton()}
</Box> </Box>
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} onClose={handleClose}> <Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={open}
onClose={handleClose}
>
<Alert <Alert
onClose={handleClose} onClose={handleClose}
severity={info?.type} severity={info?.type}
variant="filled" variant="filled"
sx={{ width: '100%' }} sx={{ width: "100%" }}
> >
{info?.message} {info?.message}
</Alert> </Alert>

View File

@ -14,7 +14,7 @@ export interface UserNameAvatar {
} }
export interface IContextProps { export interface IContextProps {
ltcBalance: number | null; foreignCoinBalance: number | null;
qortBalance: number | null; qortBalance: number | null;
userInfo: any; userInfo: any;
setUserInfo: (val: any) => void; setUserInfo: (val: any) => void;
@ -32,11 +32,14 @@ export interface IContextProps {
updateTemporaryFailedTradeBots: (val: any)=> void; updateTemporaryFailedTradeBots: (val: any)=> void;
fetchTemporarySellOrders: ()=> void; fetchTemporarySellOrders: ()=> void;
isUsingGateway: boolean; isUsingGateway: boolean;
selectedCoin: string;
setSelectedCoin: (val: any)=> void;
getCoinLabel: ()=> string | null | void;
} }
const defaultState: IContextProps = { const defaultState: IContextProps = {
qortBalance: null, qortBalance: null,
ltcBalance: null, foreignCoinBalance: null,
userInfo: null, userInfo: null,
setUserInfo: () => {}, setUserInfo: () => {},
userNameAvatar: {}, userNameAvatar: {},
@ -52,7 +55,10 @@ const defaultState: IContextProps = {
deleteTemporarySellOrder: ()=> {}, deleteTemporarySellOrder: ()=> {},
updateTemporaryFailedTradeBots: ()=> {}, updateTemporaryFailedTradeBots: ()=> {},
fetchTemporarySellOrders: ()=> {}, fetchTemporarySellOrders: ()=> {},
isUsingGateway: true isUsingGateway: true,
selectedCoin: 'LITECOIN',
setSelectedCoin: ()=> {},
getCoinLabel: ()=> {}
}; };
export default React.createContext(defaultState); export default React.createContext(defaultState);

View File

@ -1,64 +1,9 @@
import { Box, Button, Typography } from "@mui/material"; import { Box, Button, Typography } from "@mui/material";
import { styled } from "@mui/system"; import { styled } from "@mui/system";
export const BubbleBoard = styled(Box)({ type TabProp = {
position: "relative", activeTab: boolean;
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%",
});
export const MainCol = styled(Box)({ export const MainCol = styled(Box)({
display: "flex", display: "flex",
@ -120,3 +65,53 @@ export const HomeWrapper = styled(Box)({
height: "90vh", height: "90vh",
width: "100%", width: "100%",
}); });
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"
})<TabProp>(({ 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"
})<TabProp>(({ theme, activeTab }) => ({
width: "1px",
height: "25px",
margin: "0 3px",
backgroundColor: activeTab ? "transparent" : "#a4a4a5",
}));

View File

@ -1,45 +1,41 @@
import { FC, useContext, useEffect, useState } from "react"; import { useContext, useEffect, useMemo, useState } from "react";
import { AppContainer } from "../../App-styles"; import { AppContainer } from "../../App-styles";
import axios from "axios";
import { Header } from "../../components/header/Header"; import { Header } from "../../components/header/Header";
import { useLocation, useNavigate } from "react-router-dom";
import gameContext from "../../contexts/gameContext"; import gameContext from "../../contexts/gameContext";
import { Link } from "react-router-dom";
import { NotificationContext } from "../../contexts/notificationContext"; import { NotificationContext } from "../../contexts/notificationContext";
import { TradeOffers } from "../../components/Grids/TradeOffers"; import { TradeOffers } from "../../components/Grids/TradeOffers";
import { OngoingTrades } from "../../components/Grids/OngoingTrades"; 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 { TextTableTitle } from "../../components/Grids/Table-styles";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
import { ReusableModal } from "../../components/common/reusable-modal/ReusableModal"; import { Tab, TabDivider, TabsContainer, TabsRow } from "./Home-Styles";
import { OAuthButton, OAuthButtonRow } from "./Home-Styles";
import { CreateSell } from "../../components/sell/CreateSell"; import { CreateSell } from "../../components/sell/CreateSell";
import { History } from "../../components/history/History";
interface IsInstalledProps {} export const HomePage = () => {
export const HomePage: FC<IsInstalledProps> = ({}) => {
const location = useLocation();
const navigate = useNavigate();
const { const {
qortBalance, qortBalance,
ltcBalance, foreignCoinBalance,
userInfo, userInfo,
isAuthenticated,
setIsAuthenticated, setIsAuthenticated,
OAuthLoading,
setOAuthLoading, setOAuthLoading,
onGoingTrades,
selectedCoin,
} = useContext(gameContext); } = useContext(gameContext);
const { setNotification } = useContext(NotificationContext); const { setNotification } = useContext(NotificationContext);
const [mode, setMode] = useState("buy"); const [mode, setMode] = useState("buy");
const filteredOngoingTrades = useMemo(() => {
return onGoingTrades?.filter(
(item) => item?.tradeInfo?.foreignBlockchain === selectedCoin
);
}, [onGoingTrades, selectedCoin]);
const checkIfAuthenticated = async () => { const checkIfAuthenticated = async () => {
try { try {
setOAuthLoading(true); setOAuthLoading(true);
setIsAuthenticated(true); setIsAuthenticated(true);
} catch (error) { } catch (error) {
console.error(error);
} finally { } finally {
setOAuthLoading(false); setOAuthLoading(false);
} }
@ -50,14 +46,31 @@ export const HomePage: FC<IsInstalledProps> = ({}) => {
}, [userInfo?.address]); }, [userInfo?.address]);
return ( return (
<AppContainer> <>
<Header <Header
qortBalance={qortBalance} qortBalance={qortBalance}
ltcBalance={ltcBalance} foreignCoinBalance={foreignCoinBalance}
mode={mode}
setMode={setMode}
/> />
<AppContainer>
<TabsContainer>
<TabsRow>
<Tab activeTab={mode === "buy"} onClick={() => setMode("buy")}>
Buy QORT
</Tab>
{/* <TabDivider activeTab={mode === "buy" || mode === "sell"} /> */}
<Tab activeTab={mode === "sell"} onClick={() => setMode("sell")}>
Sell QORT
</Tab>
{/* <TabDivider activeTab={mode === "sell" || mode === "history"} /> */}
{/* <Tab
activeTab={mode === "history"}
onClick={() => setMode("history")}
>
Trade History
</Tab> */}
</TabsRow>
</TabsContainer>
<div <div
style={{ style={{
width: "100%", width: "100%",
@ -67,7 +80,6 @@ export const HomePage: FC<IsInstalledProps> = ({}) => {
<Spacer height="10px" /> <Spacer height="10px" />
<Box <Box
sx={{ sx={{
padding: "0 10px",
width: "100%", width: "100%",
}} }}
> >
@ -76,7 +88,7 @@ export const HomePage: FC<IsInstalledProps> = ({}) => {
fontSize: "16px", fontSize: "16px",
}} }}
> >
My Pending Orders {`My Pending Orders: ${filteredOngoingTrades?.length}`}
</TextTableTitle> </TextTableTitle>
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
@ -84,7 +96,6 @@ export const HomePage: FC<IsInstalledProps> = ({}) => {
<Spacer height="10px" /> <Spacer height="10px" />
<Box <Box
sx={{ sx={{
padding: "0 10px",
width: "100%", width: "100%",
}} }}
> >
@ -97,10 +108,12 @@ export const HomePage: FC<IsInstalledProps> = ({}) => {
</TextTableTitle> </TextTableTitle>
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
<TradeOffers ltcBalance={ltcBalance} /> <TradeOffers foreignCoinBalance={foreignCoinBalance} />
</div> </div>
<CreateSell show={mode === "sell"} qortAddress={userInfo?.address} /> <CreateSell show={mode === "sell"} qortAddress={userInfo?.address} />
<History show={mode === "history"} qortAddress={userInfo?.address} />
</AppContainer> </AppContainer>
</>
); );
}; };

View File

@ -1,6 +1,25 @@
import moment from "moment";
export function formatTime(seconds: number): string { export function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60; const remainingSeconds = seconds % 60;
// Pad the seconds with a leading zero if less than 10 // Pad the seconds with a leading zero if less than 10
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; 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');
}
}