ltc-qort
18
.eslintrc.cjs
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
release-builds/
|
||||
*.zip
|
30
README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
23
index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Qort.Trade</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
7995
package-lock.json
generated
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/material": "^5.15.14",
|
||||
"ag-grid-community": "^32.0.1",
|
||||
"ag-grid-react": "^32.0.1",
|
||||
"axios": "^1.7.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"react-toastify": "^10.0.5",
|
||||
"sass": "^1.76.0",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"socket.io-client": "^4.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-pwa": "^0.20.5"
|
||||
}
|
||||
}
|
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 834 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
26
public/manifest.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "My React App",
|
||||
"short_name": "ReactApp",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"description": "My awesome React app!",
|
||||
"theme_color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/apple-touch-icon.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
19
src/App-styles.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
export const AppContainer = styled(Box)(({ theme }) => ({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
padding: "1em 0",
|
||||
paddingBottom: '50px'
|
||||
}));
|
||||
|
||||
export const MainContainer = styled(Box)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
127
src/App.css
Normal file
@ -0,0 +1,127 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ag-theme-alpine-dark .ag-header-cell-label {
|
||||
font-size: 12px;
|
||||
font-family: 'Inter';
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
/* Define the font for the regular data */
|
||||
.ag-theme-alpine-dark .ag-cell {
|
||||
font-size: 12px;
|
||||
font-family: 'Inter';
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark .ag-header {
|
||||
background-color: #1A1D1E;
|
||||
}
|
||||
|
||||
.ag-theme-alpine .ag-row {
|
||||
background-color: #292929;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Optional: Change the background color of the even rows to the same color */
|
||||
.ag-theme-alpine-dark .ag-row:nth-child(even) {
|
||||
background-color: #292929; /* Same color as normal rows */
|
||||
}
|
||||
|
||||
/* Optional: Change the background color of the odd rows to the same color */
|
||||
.ag-theme-alpine-dark .ag-row:nth-child(odd) {
|
||||
background-color: #292929; /* Same color as normal rows */
|
||||
}
|
||||
|
||||
/* Vertically center the text in the rows */
|
||||
.ag-theme-alpine-dark .ag-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Optional: Ensure the header text is also vertically centered */
|
||||
.ag-theme-alpine-dark .ag-header-cell-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Ensure the full width of the rows has the correct background color */
|
||||
.ag-theme-alpine-dark .ag-row,
|
||||
.ag-theme-alpine-dark .ag-row-odd,
|
||||
.ag-theme-alpine-dark .ag-row-even {
|
||||
width: 100%;
|
||||
background-color: #292929 !important; /* Replace with your desired color */
|
||||
}
|
||||
|
||||
.ag-theme-alpine-dark {
|
||||
width: 100% !important;
|
||||
}
|
||||
.ag-theme-alpine-dark {
|
||||
--ag-background-color: #292929 !important;
|
||||
}
|
||||
|
||||
/* Remove any box-shadow or border from the grid cells */
|
||||
.ag-theme-alpine-dark .ag-cell,
|
||||
.ag-theme-alpine-dark .ag-header-cell,
|
||||
.ag-theme-alpine-dark .ag-header-cell-resize {
|
||||
box-shadow: none;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Remove vertical borders between cells */
|
||||
.ag-theme-alpine-dark .ag-cell {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('./assets/fonts/Inter-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('./assets/fonts/Inter-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
}
|
292
src/App.tsx
Normal file
@ -0,0 +1,292 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import "./App.css";
|
||||
import socketService from "./services/socketService";
|
||||
import GameContext, {
|
||||
IContextProps,
|
||||
UserNameAvatar,
|
||||
} from "./contexts/gameContext";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
import { darkTheme } from "./styles/theme";
|
||||
import { HomePage } from "./pages/Home/Home";
|
||||
import { UserContext, UserContextProps } from "./contexts/userContext";
|
||||
import {
|
||||
NotificationProps,
|
||||
NotificationContext,
|
||||
} from "./contexts/notificationContext";
|
||||
import { Notification } from "./components/common/notification/Notification";
|
||||
import { LoadingContext } from "./contexts/loadingContext";
|
||||
import axios from "axios";
|
||||
import { executeEvent } from "./utils/events";
|
||||
import { useIndexedDBContext } from "./contexts/indexedDBContext";
|
||||
import { useGetOngoingTransactions } from "./components/DbComponents/OngoingTransactions";
|
||||
|
||||
|
||||
|
||||
|
||||
export async function sendRequestToExtension(
|
||||
requestType: string,
|
||||
payload?: any,
|
||||
timeout: number = 20000
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = Math.random().toString(36).substring(2, 15); // Generate a unique ID for the request
|
||||
const detail = {
|
||||
type: requestType,
|
||||
payload,
|
||||
requestId,
|
||||
timeout: timeout / 1000,
|
||||
};
|
||||
|
||||
// Store the timeout ID so it can be cleared later
|
||||
const timeoutId = setTimeout(() => {
|
||||
document.removeEventListener("qortalExtensionResponses", handleResponse);
|
||||
reject(new Error("Request timed out"));
|
||||
}, timeout); // Adjust timeout as necessary
|
||||
|
||||
function handleResponse(event: any) {
|
||||
const { requestId: responseId, data } = event.detail;
|
||||
if (requestId === responseId) {
|
||||
// Match the response with the request
|
||||
document.removeEventListener(
|
||||
"qortalExtensionResponses",
|
||||
handleResponse
|
||||
);
|
||||
clearTimeout(timeoutId); // Clear the timeout upon successful response
|
||||
resolve(data);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("qortalExtensionResponses", handleResponse);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("qortalExtensionRequests", { detail })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [qortBalance, setQortBalance] = useState<any>(null);
|
||||
const [ltcBalance, setLtcBalance] = useState<any>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [OAuthLoading, setOAuthLoading] = useState<boolean>(false);
|
||||
const db = useIndexedDBContext();
|
||||
const [isUsingGateway, setIsUsingGateway] = useState(true)
|
||||
|
||||
const [isSocketUp, setIsSocketUp] = useState<boolean>(false);
|
||||
// const [onGoingTrades, setOngoingTrades] = useState([])
|
||||
const {onGoingTrades, fetchOngoingTransactions, updateTransactionInDB, deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, sellOrders} = useGetOngoingTransactions({qortAddress: userInfo?.address})
|
||||
const [userNameAvatar, setUserNameAvatar] = useState<
|
||||
Record<string, UserNameAvatar>
|
||||
>({});
|
||||
const [avatar, setAvatar] = useState<string>("");
|
||||
const [notification, setNotification] = useState<NotificationProps>({
|
||||
alertType: "",
|
||||
msg: "",
|
||||
});
|
||||
const [loadingSlider, setLoadingSlider] = useState<boolean>(false);
|
||||
|
||||
const loadingContextValue = {
|
||||
loadingSlider,
|
||||
setLoadingSlider,
|
||||
};
|
||||
|
||||
const getIsUsingGateway = async ()=> {
|
||||
try {
|
||||
const res = await qortalRequest({
|
||||
action: "IS_USING_GATEWAY"
|
||||
})
|
||||
setIsUsingGateway(res.isGateway)
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
getIsUsingGateway()
|
||||
}, [])
|
||||
|
||||
const resetNotification = () => {
|
||||
setNotification({ alertType: "", msg: "" });
|
||||
};
|
||||
|
||||
|
||||
const userContextValue: UserContextProps = {
|
||||
avatar,
|
||||
setAvatar,
|
||||
};
|
||||
|
||||
const notificationContextValue = {
|
||||
notification,
|
||||
setNotification,
|
||||
resetNotification,
|
||||
};
|
||||
|
||||
async function getNameInfo(address: string) {
|
||||
const response = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: address,
|
||||
});
|
||||
const nameData = response;
|
||||
|
||||
if (nameData?.length > 0) {
|
||||
return nameData[0].name;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const askForAccountInformation = React.useCallback(async () => {
|
||||
try {
|
||||
const account = await qortalRequest({
|
||||
action: "GET_USER_ACCOUNT",
|
||||
});
|
||||
|
||||
const name = await getNameInfo(account.address);
|
||||
setUserInfo({ ...account, name });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
askForAccountInformation();
|
||||
}, [askForAccountInformation]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const getQortBalance = async ()=> {
|
||||
const balanceUrl: string = `/addresses/balance/${userInfo?.address}`;
|
||||
const balanceResponse = await axios(balanceUrl);
|
||||
setQortBalance(balanceResponse.data?.value)
|
||||
}
|
||||
|
||||
const getLTCBalance = async () => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "GET_WALLET_BALANCE",
|
||||
coin: "LTC"
|
||||
});
|
||||
if(!response?.error){
|
||||
setLtcBalance(+response)
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(!userInfo?.address) return
|
||||
const intervalGetTradeInfo = setInterval(() => {
|
||||
fetchOngoingTransactions()
|
||||
getLTCBalance()
|
||||
getQortBalance()
|
||||
}, 150000)
|
||||
getLTCBalance()
|
||||
getQortBalance()
|
||||
return () => {
|
||||
clearInterval(intervalGetTradeInfo)
|
||||
}
|
||||
}, [userInfo?.address, isAuthenticated])
|
||||
|
||||
|
||||
const handleMessage = async (event: any) => {
|
||||
if (event.data.type === "LOGOUT") {
|
||||
console.log("Logged out from extension");
|
||||
setUserInfo(null);
|
||||
setAvatar("");
|
||||
setIsAuthenticated(false);
|
||||
setQortBalance(null)
|
||||
setLtcBalance(null)
|
||||
localStorage.setItem("token", "");
|
||||
} else if(event.data.type === "RESPONSE_FOR_TRADES"){
|
||||
|
||||
|
||||
const response = event.data.payload
|
||||
if (response?.extra?.atAddresses
|
||||
) {
|
||||
try {
|
||||
const status = response.callResponse === true ? 'trade-ongoing' : 'trade-failed'
|
||||
const token = localStorage.getItem("token");
|
||||
// Prepare transaction data
|
||||
const transactionData = {
|
||||
qortalAtAddresses: response.extra.atAddresses,
|
||||
qortAddress: userInfo.address,
|
||||
status: status,
|
||||
message: response.extra.message,
|
||||
};
|
||||
|
||||
// Update transactions in IndexedDB
|
||||
const result = await updateTransactionInDB(transactionData);
|
||||
fetchOngoingTransactions()
|
||||
executeEvent("execute-get-new-block-trades", {})
|
||||
} catch (error) {
|
||||
console.log({error})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [userInfo?.address]);
|
||||
|
||||
const gameContextValue: IContextProps = {
|
||||
userInfo,
|
||||
setUserInfo,
|
||||
userNameAvatar,
|
||||
setUserNameAvatar,
|
||||
onGoingTrades,
|
||||
fetchOngoingTransactions,
|
||||
ltcBalance,
|
||||
qortBalance,
|
||||
isAuthenticated,
|
||||
setIsAuthenticated,
|
||||
OAuthLoading,
|
||||
setOAuthLoading,
|
||||
updateTransactionInDB,
|
||||
sellOrders,
|
||||
deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, isUsingGateway
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={notificationContextValue}>
|
||||
<LoadingContext.Provider value={loadingContextValue}>
|
||||
<UserContext.Provider value={userContextValue}>
|
||||
<GameContext.Provider value={gameContextValue}>
|
||||
<Notification />
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<HomePage
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
</GameContext.Provider>
|
||||
</UserContext.Provider>
|
||||
</LoadingContext.Provider>
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
4
src/assets/SVG/LOGO.svg
Normal file
After Width: | Height: | Size: 17 KiB |
3
src/assets/SVG/caretDown.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="15" height="8" viewBox="0 0 15 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1L7.5 7L14 1" stroke="#464646" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 169 B |
1
src/assets/SVG/closeIcon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z"/></svg>
|
After Width: | Height: | Size: 201 B |
1
src/assets/SVG/doubleCaretRight.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M383-480 200-664l56-56 240 240-240 240-56-56 183-184Zm264 0L464-664l56-56 240 240-240 240-56-56 183-184Z"/></svg>
|
After Width: | Height: | Size: 229 B |
3
src/assets/SVG/home.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="19" viewBox="0 0 21 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.64729 18.9972C3.27267 18.9972 2.96761 18.685 2.96761 18.2955V10.7916H1.18276H1.18009C0.527159 10.7916 0 10.2446 0 9.57322C0 9.18918 0.173937 8.84659 0.44153 8.6228L2.75086 6.5562V3.11371C2.75086 2.71034 3.06662 2.38432 3.45463 2.38432H4.91569C5.30637 2.38432 5.62213 2.71034 5.62213 3.11371V3.98124L9.72701 0.298386C10.1712 -0.099462 10.8295 -0.099462 11.2737 0.298386L20.594 8.65319C20.9659 8.98749 21.0971 9.52348 20.9258 10.0015C20.7519 10.4767 20.313 10.7944 19.8206 10.7944H18.0331V18.2982C18.0331 18.6878 17.7281 19 17.3534 19H13.3368V13.5406C13.3368 12.7698 12.7321 12.1454 11.9855 12.1454H9.01254C8.26595 12.1454 7.66119 12.7698 7.66119 13.5406V18.9945H3.64729V18.9972Z" fill="#464646"/>
|
||||
</svg>
|
After Width: | Height: | Size: 811 B |
3
src/assets/SVG/info.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 0C5.61553 0 4.26215 0.410543 3.11101 1.17971C1.95987 1.94888 1.06266 3.04213 0.532846 4.32121C0.00303299 5.6003 -0.13559 7.00776 0.134506 8.36563C0.404603 9.7235 1.07129 10.9708 2.05026 11.9497C3.02922 12.9287 4.2765 13.5954 5.63437 13.8655C6.99224 14.1356 8.3997 13.997 9.67879 13.4672C10.9579 12.9373 12.0511 12.0401 12.8203 10.889C13.5895 9.73784 14 8.38447 14 7C13.9979 5.14412 13.2598 3.36484 11.9475 2.05253C10.6352 0.740225 8.85588 0.002064 7 0ZM7 12.6C5.89243 12.6 4.80972 12.2716 3.88881 11.6562C2.96789 11.0409 2.25013 10.1663 1.82628 9.14302C1.40243 8.11976 1.29153 6.99379 1.50761 5.90749C1.72368 4.8212 2.25703 3.82337 3.0402 3.0402C3.82338 2.25703 4.8212 1.72368 5.9075 1.5076C6.99379 1.29153 8.11976 1.40242 9.14303 1.82627C10.1663 2.25012 11.0409 2.96789 11.6562 3.88881C12.2716 4.80972 12.6 5.89242 12.6 7C12.5983 8.48469 12.0078 9.90808 10.9579 10.9579C9.90809 12.0078 8.48469 12.5983 7 12.6ZM7.7 6.3V9.8C7.7 9.98565 7.62625 10.1637 7.49498 10.295C7.3637 10.4262 7.18565 10.5 7 10.5C6.81435 10.5 6.6363 10.4262 6.50503 10.295C6.37375 10.1637 6.3 9.98565 6.3 9.8V7C6.11435 7 5.9363 6.92625 5.80503 6.79497C5.67375 6.6637 5.6 6.48565 5.6 6.3C5.6 6.11435 5.67375 5.9363 5.80503 5.80502C5.9363 5.67375 6.11435 5.6 6.3 5.6H7C7.18565 5.6 7.3637 5.67375 7.49498 5.80502C7.62625 5.9363 7.7 6.11435 7.7 6.3ZM7.7 4.2C7.7 4.33845 7.65895 4.47378 7.58203 4.5889C7.50511 4.70401 7.39579 4.79373 7.26788 4.84671C7.13997 4.8997 6.99923 4.91356 6.86344 4.88655C6.72765 4.85954 6.60292 4.79287 6.50503 4.69497C6.40713 4.59708 6.34046 4.47235 6.31345 4.33656C6.28644 4.20078 6.3003 4.06003 6.35329 3.93212C6.40627 3.80421 6.49599 3.69489 6.6111 3.61797C6.72622 3.54105 6.86156 3.5 7 3.5C7.18565 3.5 7.3637 3.57375 7.49498 3.70502C7.62625 3.8363 7.7 4.01435 7.7 4.2Z" fill="#464646"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
13
src/assets/SVG/qort-white.svg
Normal file
After Width: | Height: | Size: 593 KiB |
13
src/assets/SVG/qort.svg
Normal file
After Width: | Height: | Size: 562 KiB |
3
src/assets/SVG/star.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="36" height="34" viewBox="0 0 36 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.7818 21.7029C28.6179 21.8647 28.542 22.094 28.5799 22.3203L29.8898 31.6695C29.993 32.4158 29.6727 33.1591 29.055 33.6026C28.4388 34.0462 27.6252 34.1211 26.9362 33.7945L18.3259 29.6705C18.1195 29.5716 17.8797 29.5716 17.6732 29.6705L9.06295 33.7945C8.37389 34.1241 7.55731 34.0537 6.93652 33.6086C6.31725 33.1636 5.99701 32.4188 6.10176 31.6695L7.41161 22.3203C7.44955 22.094 7.37366 21.8647 7.20974 21.7029L0.592327 14.9012C0.0520072 14.3573 -0.136207 13.5616 0.100576 12.8378C0.337351 12.1125 0.962693 11.5775 1.72309 11.4486L11.1334 9.79128C11.3626 9.75381 11.5584 9.61145 11.6631 9.40765L16.1711 1.08343H16.1696C16.5308 0.416576 17.2335 0 18 0C18.7665 0 19.4692 0.416576 19.8304 1.08343L24.3384 9.40765H24.3369C24.4416 9.61145 24.6374 9.75381 24.8666 9.79128L34.2769 11.4486C35.0373 11.5775 35.6626 12.1125 35.8994 12.8378C36.1362 13.5616 35.948 14.3573 35.4077 14.9012L28.7818 21.7029Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
src/assets/SVG/volumeOff.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Zm-80 238v-94l-72-72H200v80h114l86 86Zm-36-130Z"/></svg>
|
After Width: | Height: | Size: 495 B |
1
src/assets/SVG/volumeOn.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z"/></svg>
|
After Width: | Height: | Size: 378 B |
BIN
src/assets/fonts/FiraSans-Medium.ttf
Normal file
BIN
src/assets/fonts/FiraSans-Regular.ttf
Normal file
BIN
src/assets/fonts/Fredoka One.ttf
Normal file
BIN
src/assets/fonts/Inter-Medium.ttf
Normal file
BIN
src/assets/fonts/Inter-SemiBold.ttf
Normal file
BIN
src/assets/fonts/Inter.ttf
Normal file
261
src/components/DbComponents/OngoingTransactions.tsx
Normal file
@ -0,0 +1,261 @@
|
||||
// src/hooks/useGetOngoingTransactions.js
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useIndexedDBContext } from "../../contexts/indexedDBContext";
|
||||
|
||||
const fetchTradeInfo = async (qortalAtAddress) => {
|
||||
|
||||
const checkIfOfferingRes = await fetch(`http://127.0.0.1:12391/crosschain/trade/${qortalAtAddress}`)
|
||||
const data = await checkIfOfferingRes.json()
|
||||
return data
|
||||
};
|
||||
|
||||
export const useGetOngoingTransactions = ({ qortAddress }) => {
|
||||
const db = useIndexedDBContext();
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [sellOrders, setSellOrders] = useState([]);
|
||||
const [sellOrderAts, setSellOrderAts] = useState([]);
|
||||
|
||||
// Fetch transactions updated within the last 22 minutes
|
||||
const fetchOngoingTransactions = useCallback(async () => {
|
||||
if (!db || !qortAddress) return;
|
||||
|
||||
try {
|
||||
const transactionStore = db.transaction("transactions", "readonly").objectStore("transactions");
|
||||
const index = transactionStore.index("updatedAt");
|
||||
const now = Date.now() - 120 * 60 * 1000; // 22 minutes ago
|
||||
|
||||
const results: any[] = await new Promise((resolve, reject) => {
|
||||
const data = [];
|
||||
index.openCursor(IDBKeyRange.lowerBound(now)).onsuccess = async (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
const transaction = cursor.value;
|
||||
if (transaction.qortAddress === qortAddress) {
|
||||
data.push(transaction);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
const results = [];
|
||||
for (const transaction of data || []) {
|
||||
const tradeData = await fetchTradeInfo(transaction.qortalAtAddress);
|
||||
if(tradeData.qortalPartnerReceivingAddress && tradeData.qortalPartnerReceivingAddress !== qortAddress) continue
|
||||
let newStatus = transaction.status
|
||||
if(tradeData.qortalPartnerReceivingAddress && tradeData.qortalPartnerReceivingAddress === qortAddress){
|
||||
newStatus = tradeData.mode.toLowerCase()
|
||||
}
|
||||
results.push({...transaction, tradeInfo: tradeData, status: newStatus});
|
||||
}
|
||||
resolve(results);
|
||||
}
|
||||
};
|
||||
index.openCursor().onerror = (event) => reject(event.target.error);
|
||||
});
|
||||
|
||||
setTransactions(results.sort((a, b) => b.createdAt - a.createdAt)); // Sort by createdAt descending
|
||||
} catch (error) {
|
||||
console.error("Error fetching ongoing transactions:", error);
|
||||
}
|
||||
}, [db, qortAddress]);
|
||||
|
||||
// Upsert transactions into IndexedDB
|
||||
const updateTransactionInDB = useCallback(async ({
|
||||
qortalAtAddresses,
|
||||
qortAddress,
|
||||
node,
|
||||
status,
|
||||
message = '',
|
||||
encryptedMessageToBase58 = undefined,
|
||||
chatSignature = '',
|
||||
sender = '',
|
||||
senderPublicKey = '',
|
||||
reference = ''
|
||||
}: any) => {
|
||||
if (!db || !qortalAtAddresses || !qortAddress) return;
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transactionStore = db.transaction("transactions", "readwrite").objectStore("transactions");
|
||||
const updatedTransactions = [];
|
||||
|
||||
qortalAtAddresses.forEach((qortalAtAddress) => {
|
||||
const request = transactionStore.get(qortalAtAddress);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const existingTransaction = event.target.result;
|
||||
|
||||
// Prepare the new or updated transaction object
|
||||
const newTransaction = {
|
||||
qortalAtAddress,
|
||||
qortAddress,
|
||||
node,
|
||||
status,
|
||||
message,
|
||||
encryptedMessageToBase58,
|
||||
chatSignature,
|
||||
sender,
|
||||
senderPublicKey,
|
||||
reference,
|
||||
updatedAt: Date.now(),
|
||||
createdAt: existingTransaction ? existingTransaction.createdAt : Date.now(), // Preserve createdAt if it exists
|
||||
};
|
||||
|
||||
transactionStore.put(newTransaction); // Upsert: create if not exists, update if exists
|
||||
updatedTransactions.push(newTransaction);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error("Error fetching transaction:", event.target.error);
|
||||
};
|
||||
});
|
||||
|
||||
transactionStore.transaction.oncomplete = () => resolve(updatedTransactions);
|
||||
transactionStore.transaction.onerror = (error) => reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating transactions:", error);
|
||||
}
|
||||
}, [db, qortAddress]);
|
||||
|
||||
|
||||
// Fetch transactions with a specific status and signature
|
||||
const fetchTemporarySellOrders = useCallback(async () => {
|
||||
if (!db || !qortAddress) return;
|
||||
|
||||
try {
|
||||
const transactionStore = db.transaction("temporarySellOrders", "readonly").objectStore("temporarySellOrders");
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const data = [];
|
||||
const request = transactionStore.openCursor();
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
|
||||
if (cursor) {
|
||||
const transaction = cursor.value;
|
||||
// Manually filter by qortAddress
|
||||
if (transaction.qortAddress === qortAddress) {
|
||||
data.push(transaction);
|
||||
}
|
||||
|
||||
cursor.continue();
|
||||
} else {
|
||||
// Deduplicate transactions by atAddress, prioritizing non-temporary ones
|
||||
const uniqueTransactions = new Map();
|
||||
[...sellOrderAts, ...data].forEach((transaction) => {
|
||||
const key = transaction.atAddress;
|
||||
|
||||
if (uniqueTransactions.has(key)) {
|
||||
const existingTransaction = uniqueTransactions.get(key);
|
||||
|
||||
// Keep the transaction that does not have 'isTemp' set to true
|
||||
if (!existingTransaction.isTemp) return;
|
||||
}
|
||||
|
||||
uniqueTransactions.set(key, transaction);
|
||||
});
|
||||
|
||||
// Sort by createdAt in descending order and update the state
|
||||
setSellOrders(
|
||||
Array.from(uniqueTransactions.values()).sort((a, b) => b.createdAt - a.createdAt)
|
||||
);
|
||||
|
||||
resolve(Array.from(uniqueTransactions.values())); // Return deduplicated results
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (event) => reject(event.target.error);
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions by qortAddress:", error);
|
||||
}
|
||||
}, [db, sellOrderAts, qortAddress]);
|
||||
|
||||
|
||||
|
||||
|
||||
const updateTemporaryFailedTradeBots = useCallback(async ({
|
||||
atAddress,
|
||||
status,
|
||||
qortAddress,
|
||||
...props
|
||||
}) => {
|
||||
if (!db || !atAddress || !qortAddress) return;
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transactionStore = db.transaction("temporarySellOrders", "readwrite").objectStore("temporarySellOrders");
|
||||
|
||||
const request = transactionStore.get(atAddress);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const existingTransaction = event.target.result;
|
||||
|
||||
// Prepare the new or updated transaction object
|
||||
const newTransaction = {
|
||||
atAddress,
|
||||
status,
|
||||
isTemp: true,
|
||||
createdAt: existingTransaction ? existingTransaction.createdAt : Date.now(),
|
||||
qortAddress,
|
||||
...props
|
||||
};
|
||||
|
||||
transactionStore.put(newTransaction); // Upsert
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error("Error fetching transaction:", event.target.error);
|
||||
reject(event.target.error);
|
||||
};
|
||||
|
||||
transactionStore.transaction.oncomplete = () => {
|
||||
resolve(true)
|
||||
};
|
||||
transactionStore.transaction.onerror = (error) => reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating transaction in IndexedDB:", error);
|
||||
}
|
||||
}, [db, qortAddress]);
|
||||
|
||||
const deleteTemporarySellOrder = useCallback(async (atAddress) => {
|
||||
if (!db || !atAddress) return;
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const transactionStore = db.transaction("temporarySellOrders", "readwrite").objectStore("temporarySellOrders");
|
||||
|
||||
const request = transactionStore.delete(atAddress);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(true); // Resolve with success
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
console.error("Error deleting transaction:", event.target.error);
|
||||
reject(event.target.error); // Reject with error
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting transaction:", error);
|
||||
}
|
||||
}, [db, qortAddress]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
fetchOngoingTransactions();
|
||||
}, [fetchOngoingTransactions]);
|
||||
|
||||
useEffect(()=> {
|
||||
fetchTemporarySellOrders()
|
||||
}, [fetchTemporarySellOrders])
|
||||
|
||||
|
||||
return { onGoingTrades: transactions, fetchOngoingTransactions, updateTransactionInDB, deleteTemporarySellOrder, updateTemporaryFailedTradeBots, fetchTemporarySellOrders, sellOrders };
|
||||
};
|
92
src/components/Grids/OngoingTrades.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { ColDef, SizeColumnsToContentStrategy } from 'ag-grid-community';
|
||||
import 'ag-grid-community/styles/ag-grid.css';
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import gameContext from '../../contexts/gameContext';
|
||||
|
||||
const autoSizeStrategy: SizeColumnsToContentStrategy = {
|
||||
type: 'fitCellContents'
|
||||
};
|
||||
|
||||
export const OngoingTrades = () => {
|
||||
const { onGoingTrades } = useContext(gameContext);
|
||||
|
||||
|
||||
const defaultColDef = {
|
||||
resizable: true, // Make columns resizable by default
|
||||
sortable: true, // Make columns sortable by default
|
||||
suppressMovable: true, // Prevent columns from being movable
|
||||
};
|
||||
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
headerName: "Status", valueGetter: (params) => {
|
||||
if (params.data.tradeInfo.mode !== 'OFFERING') {
|
||||
if (params.data.tradeInfo.mode === 'TRADING') return 'Trading'
|
||||
if (params.data.tradeInfo.mode === 'REDEEMED') return 'Completed'
|
||||
return params.data.tradeInfo.mode.toLowerCase()
|
||||
}
|
||||
if (params.data.status === 'message-sent') return 'Requested'
|
||||
if (params.data.status === 'trade-ongoing') return 'Submitted'
|
||||
if (params.data.status === 'trade-failed') return 'Failed'
|
||||
return params.data.status
|
||||
},
|
||||
resizable: true ,
|
||||
flex: 1, minWidth: 100
|
||||
},
|
||||
{ headerName: "Amount (QORT)", valueGetter: (params) => +params.data.tradeInfo.qortAmount, resizable: true, flex: 1, minWidth: 100 },
|
||||
{ headerName: "LTC/QORT", valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount / +params.data.tradeInfo.qortAmount , resizable: true , flex: 1, minWidth: 100},
|
||||
{ headerName: "Total LTC Value", valueGetter: (params) => +params.data.tradeInfo.expectedForeignAmount, resizable: true , flex: 1, minWidth: 100 },
|
||||
{
|
||||
headerName: "Notes", valueGetter: (params) => {
|
||||
if (params.data.tradeInfo.mode === 'TRADING') {
|
||||
return 'The order is in the process of exchanging hands. This does not necessary mean it was purchased by your account. Wait until the process is completed.'
|
||||
}
|
||||
if (params.data.tradeInfo.mode === 'REDEEMED') {
|
||||
return "You have successfully purchased this order. Please wait for the QORT balance to be updated"
|
||||
}
|
||||
if (params.data.status === 'message-sent') {
|
||||
return 'Buy request was sent, waiting for trade confirmation.'
|
||||
}
|
||||
if (params?.data?.message?.toLowerCase() === 'invalid search criteria') {
|
||||
return 'Order(s) already taken';
|
||||
}
|
||||
|
||||
if (params.data.message) return params.data.message
|
||||
}, resizable: true, flex: 1, minWidth: 100
|
||||
}
|
||||
];
|
||||
|
||||
// const getRowStyle = (params: any) => {
|
||||
// if (params.data.qortalAtAddress === selectedOffer?.qortalAtAddress) {
|
||||
// return { background: 'lightblue' };
|
||||
// }
|
||||
// return null;
|
||||
// };
|
||||
const getRowId = useCallback(function (params: any) {
|
||||
return String(params.data._id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="ag-theme-alpine-dark" style={{ height: 225, width: '100%' }}>
|
||||
<AgGridReact
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={onGoingTrades}
|
||||
// onRowClicked={onRowClicked}
|
||||
rowSelection="single"
|
||||
getRowId={getRowId}
|
||||
autoSizeStrategy={autoSizeStrategy}
|
||||
suppressHorizontalScroll={false} // Allow horizontal scroll on mobile if needed
|
||||
suppressCellFocus={true} // Prevents cells from stealing focus in mobile
|
||||
// pagination={true}
|
||||
// paginationPageSize={10}
|
||||
// domLayout='autoHeight'
|
||||
|
||||
// getRowStyle={getRowStyle}
|
||||
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
11
src/components/Grids/Table-styles.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
export const TextTableTitle = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Inter",
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 400,
|
||||
fontSize: "20px",
|
||||
lineHeight: "40px",
|
||||
userSelect: "none",
|
||||
}));
|
575
src/components/Grids/TradeOffers.tsx
Normal file
@ -0,0 +1,575 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { ColDef, RowClassParams, RowStyle, SizeColumnsToContentStrategy } from 'ag-grid-community';
|
||||
import 'ag-grid-community/styles/ag-grid.css';
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import axios from 'axios';
|
||||
import { sendRequestToExtension } from '../../App';
|
||||
import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Snackbar, SnackbarCloseReason, Typography } from '@mui/material';
|
||||
import gameContext from '../../contexts/gameContext';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
import { useModal } from '../common/useModal';
|
||||
import FileSaver from 'file-saver';
|
||||
|
||||
interface RowData {
|
||||
amountQORT: number;
|
||||
priceUSD: number;
|
||||
totalUSD: number;
|
||||
seller: string;
|
||||
}
|
||||
|
||||
export const saveFileToDisk = async (data) => {
|
||||
|
||||
const dataString = JSON.stringify(data);
|
||||
const blob = new Blob([dataString], { type: 'application/json' });
|
||||
const fileName = "traderecord_" + Date.now() + '_' + ".json";
|
||||
|
||||
await FileSaver.saveAs(blob, fileName);
|
||||
|
||||
}
|
||||
|
||||
export const autoSizeStrategy: SizeColumnsToContentStrategy = {
|
||||
type: 'fitCellContents'
|
||||
};
|
||||
|
||||
export const TradeOffers: React.FC<any> = ({ltcBalance}:any) => {
|
||||
const [offers, setOffers] = useState<any[]>([])
|
||||
|
||||
const { fetchOngoingTransactions, onGoingTrades, updateTransactionInDB, isUsingGateway } = useContext(gameContext);
|
||||
const listOfOngoingTradesAts = useMemo(()=> {
|
||||
return onGoingTrades?.filter((item)=> item?.status !== 'trade-failed')?.map((trade)=> trade?.qortalAtAddress) || []
|
||||
}, [onGoingTrades])
|
||||
const {
|
||||
isShow: isShowInfo,
|
||||
onCancel: onCancelInfo,
|
||||
onOk: onOkInfo,
|
||||
show: showInfo,
|
||||
message: messageInfo,
|
||||
} = useModal();
|
||||
|
||||
const offersWithoutOngoing = useMemo(()=> {
|
||||
return offers.filter((item)=> !listOfOngoingTradesAts.includes(item.qortalAtAddress))
|
||||
}, [listOfOngoingTradesAts, offers])
|
||||
|
||||
|
||||
|
||||
const [selectedOffer, setSelectedOffer] = useState<any>(null)
|
||||
const [selectedOffers, setSelectedOffers] = useState<any>([])
|
||||
const [record, setRecord] = useState(null)
|
||||
const tradePresenceTxns = useRef<any[]>([])
|
||||
const offeringTrades = useRef<any[]>([])
|
||||
const blockedTradesList = useRef([])
|
||||
const gridRef = useRef<any>(null)
|
||||
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [info, setInfo] = useState<any>(null)
|
||||
const BuyButton = () => {
|
||||
return (
|
||||
<button onClick={buyOrder} style={{borderRadius: '8px', width: '74px', height:"30px", background: "#4D7345",
|
||||
color: 'white', cursor: 'pointer', border: '1px solid #375232', boxShadow: '0px 2.77px 2.21px 0px #00000005'
|
||||
}}>
|
||||
BUY
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultColDef = {
|
||||
resizable: true, // Make columns resizable by default
|
||||
sortable: true, // Make columns sortable by default
|
||||
suppressMovable: true, // Prevent columns from being movable
|
||||
};
|
||||
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
headerCheckboxSelection: true, // Adds a checkbox in the header for selecting all rows
|
||||
checkboxSelection: true, // Adds checkboxes in each row for selection
|
||||
headerName: "Select", // You can customize the header name
|
||||
width: 50, // Adjust the width as needed
|
||||
pinned: 'left', // Optional, to pin this column on the left
|
||||
resizable: false,
|
||||
},
|
||||
{ headerName: "QORT AMOUNT", field: "qortAmount" , flex: 1, // Flex makes this column responsive
|
||||
minWidth: 150, // Ensure it doesn't shrink too much
|
||||
resizable: true },
|
||||
{ headerName: "LTC/QORT", valueGetter: (params) => +params.data.foreignAmount / +params.data.qortAmount, sortable: true, sort: 'asc', flex: 1, // Flex makes this column responsive
|
||||
minWidth: 150, // Ensure it doesn't shrink too much
|
||||
resizable: true },
|
||||
{ headerName: "Total LTC Value", field: "foreignAmount", flex: 1, // Flex makes this column responsive
|
||||
minWidth: 150, // Ensure it doesn't shrink too much
|
||||
resizable: true },
|
||||
{ headerName: "Seller", field: "qortalCreator", flex: 1, // Flex makes this column responsive
|
||||
minWidth: 300, // Ensure it doesn't shrink too much
|
||||
resizable: true },
|
||||
];
|
||||
|
||||
|
||||
|
||||
// const onRowClicked = (event: any) => {
|
||||
// if(listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return
|
||||
// setSelectedOffer(event.data)
|
||||
|
||||
// };
|
||||
|
||||
const restartTradePresenceWebSocket = () => {
|
||||
setTimeout(() => initTradePresenceWebSocket(true), 50)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getNewBlockedTrades = async () => {
|
||||
const unconfirmedTransactionsList = async () => {
|
||||
|
||||
const unconfirmedTransactionslUrl = `/transactions/unconfirmed?txType=MESSAGE&limit=0&reverse=true`
|
||||
|
||||
var addBlockedTrades = JSON.parse(localStorage.getItem('failedTrades') || '[]')
|
||||
|
||||
await fetch(unconfirmedTransactionslUrl).then(response => {
|
||||
return response.json()
|
||||
}).then(data => {
|
||||
data.map((item: any) => {
|
||||
const unconfirmedNessageTimeDiff = Date.now() - item.timestamp
|
||||
const timeOneHour = 60 * 60 * 1000
|
||||
if (Number(unconfirmedNessageTimeDiff) > Number(timeOneHour)) {
|
||||
const addBlocked = {
|
||||
timestamp: item.timestamp,
|
||||
recipient: item.recipient
|
||||
}
|
||||
addBlockedTrades.push(addBlocked)
|
||||
}
|
||||
})
|
||||
localStorage.setItem("failedTrades", JSON.stringify(addBlockedTrades))
|
||||
blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]')
|
||||
})
|
||||
}
|
||||
|
||||
await unconfirmedTransactionsList()
|
||||
|
||||
const filterUnconfirmedTransactionsList = async () => {
|
||||
let cleanBlockedTrades = blockedTradesList.current.reduce((newArray, cut: any) => {
|
||||
if (cut && !newArray.some((obj: any) => obj.recipient === cut.recipient)) {
|
||||
newArray.push(cut)
|
||||
}
|
||||
return newArray
|
||||
}, [] as any[])
|
||||
localStorage.setItem("failedTrades", JSON.stringify(cleanBlockedTrades))
|
||||
blockedTradesList.current = JSON.parse(localStorage.getItem("failedTrades") || "[]")
|
||||
}
|
||||
|
||||
await filterUnconfirmedTransactionsList()
|
||||
processOffersWithPresence()
|
||||
}
|
||||
|
||||
const executeGetNewBlockTrades = useCallback(()=> {
|
||||
getNewBlockedTrades()
|
||||
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("execute-get-new-block-trades", executeGetNewBlockTrades);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("execute-get-new-block-trades", executeGetNewBlockTrades);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const processOffersWithPresence = () => {
|
||||
if (offeringTrades.current === null) return
|
||||
async function asyncForEach(array: any, callback: any) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const filterOffersUsingTradePresence = (offeringTrade: any) => {
|
||||
return offeringTrade.tradePresenceExpiry > Date.now();
|
||||
}
|
||||
|
||||
const startOfferPresenceMapping = async () => {
|
||||
if (tradePresenceTxns.current) {
|
||||
for (const tradePresence of tradePresenceTxns.current) {
|
||||
const offerIndex = offeringTrades.current.findIndex(offeringTrade => offeringTrade.qortalCreatorTradeAddress === tradePresence.tradeAddress);
|
||||
if (offerIndex !== -1) {
|
||||
offeringTrades.current[offerIndex].tradePresenceExpiry = tradePresence.timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filteredOffers = offeringTrades.current?.filter((offeringTrade) => filterOffersUsingTradePresence(offeringTrade)) || []
|
||||
let tradesPresenceCleaned: any[] = filteredOffers
|
||||
|
||||
|
||||
blockedTradesList.current.forEach((item: any) => {
|
||||
const toDelete = item.recipient
|
||||
tradesPresenceCleaned = tradesPresenceCleaned?.filter(el => {
|
||||
return el.qortalCreatorTradeAddress !== toDelete
|
||||
}) || []
|
||||
})
|
||||
|
||||
if (tradesPresenceCleaned) {
|
||||
updateGridData(tradesPresenceCleaned)
|
||||
}
|
||||
}
|
||||
|
||||
startOfferPresenceMapping()
|
||||
}
|
||||
|
||||
const restartTradeOffersWebSocket = () => {
|
||||
setTimeout(() => initTradeOffersWebSocket(true), 50)
|
||||
}
|
||||
|
||||
const initTradePresenceWebSocket = (restarted = false) => {
|
||||
let socketTimeout: any
|
||||
let socketLink
|
||||
if(isUsingGateway){
|
||||
socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradepresence`
|
||||
} else {
|
||||
socketLink = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websockets/crosschain/tradepresence`;
|
||||
|
||||
}
|
||||
|
||||
|
||||
const socket = new WebSocket(socketLink)
|
||||
socket.onopen = () => {
|
||||
setTimeout(pingSocket, 50)
|
||||
}
|
||||
socket.onmessage = (e) => {
|
||||
tradePresenceTxns.current = JSON.parse(e.data)
|
||||
processOffersWithPresence()
|
||||
restarted = false
|
||||
}
|
||||
socket.onclose = () => {
|
||||
clearTimeout(socketTimeout)
|
||||
restartTradePresenceWebSocket()
|
||||
}
|
||||
socket.onerror = (e) => {
|
||||
clearTimeout(socketTimeout)
|
||||
}
|
||||
const pingSocket = () => {
|
||||
socket.send('ping')
|
||||
socketTimeout = setTimeout(pingSocket, 295000)
|
||||
}
|
||||
}
|
||||
|
||||
const initTradeOffersWebSocket = (restarted = false) => {
|
||||
let tradeOffersSocketCounter = 0
|
||||
let socketTimeout: any
|
||||
|
||||
let socketLink
|
||||
if(isUsingGateway){
|
||||
socketLink = `wss://appnode.qortal.org/websockets/crosschain/tradeoffers?foreignBlockchain=LITECOIN&includeHistoric=true`
|
||||
} else {
|
||||
socketLink = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websockets/crosschain/tradeoffers?foreignBlockchain=LITECOIN&includeHistoric=true`
|
||||
|
||||
}
|
||||
const socket = new WebSocket(socketLink)
|
||||
socket.onopen = () => {
|
||||
setTimeout(pingSocket, 50)
|
||||
tradeOffersSocketCounter += 1
|
||||
}
|
||||
socket.onmessage = (e) => {
|
||||
offeringTrades.current = [...offeringTrades.current, ...JSON.parse(e.data)]
|
||||
tradeOffersSocketCounter += 1
|
||||
restarted = false
|
||||
processOffersWithPresence()
|
||||
}
|
||||
socket.onclose = () => {
|
||||
clearTimeout(socketTimeout)
|
||||
restartTradeOffersWebSocket()
|
||||
}
|
||||
socket.onerror = (e) => {
|
||||
clearTimeout(socketTimeout)
|
||||
}
|
||||
const pingSocket = () => {
|
||||
socket.send('ping')
|
||||
socketTimeout = setTimeout(pingSocket, 295000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]')
|
||||
initTradePresenceWebSocket()
|
||||
initTradeOffersWebSocket()
|
||||
getNewBlockedTrades()
|
||||
const intervalBlockTrades = setInterval(() => {
|
||||
getNewBlockedTrades()
|
||||
}, 150000)
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalBlockTrades)
|
||||
}
|
||||
}, [isUsingGateway])
|
||||
|
||||
|
||||
|
||||
const selectedTotalLTC = useMemo(() => {
|
||||
return selectedOffers.reduce((acc: number, curr: any) => {
|
||||
return acc + (+curr.foreignAmount || 0); // Ensure qortAmount is defined
|
||||
}, 0);
|
||||
}, [selectedOffers]);
|
||||
|
||||
|
||||
const buyOrder = async () => {
|
||||
try {
|
||||
if(+ltcBalance < +selectedTotalLTC.toFixed(4)){
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: "You don't have enough LTC or your balance was not retrieved"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedOffers?.length < 1) return
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'info',
|
||||
message: "Attempting to submit buy order. Please wait..."
|
||||
})
|
||||
const listOfATs = selectedOffers
|
||||
|
||||
const response = await qortalRequestWithTimeout({
|
||||
action: "CREATE_TRADE_BUY_ORDER",
|
||||
crosschainAtInfo: listOfATs,
|
||||
foreignBlockchain: 'LITECOIN'
|
||||
}, 900000);
|
||||
|
||||
if(response?.error){
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: response?.error || "Failed to submit trade order."
|
||||
})
|
||||
return
|
||||
}
|
||||
if (response?.extra?.atAddresses) {
|
||||
setSelectedOffers([])
|
||||
const transactionData = {
|
||||
qortalAtAddresses: response?.extra?.atAddresses,
|
||||
qortAddress: response?.extra?.senderAddress,
|
||||
node: response?.extra?.node,
|
||||
status:response?.extra?.status ? response?.extra?.status : response.callResponse === true ? 'trade-ongoing' : 'trade-failed',
|
||||
encryptedMessageToBase58: response?.encryptedMessageToBase58,
|
||||
chatSignature: response?.chatSignature,
|
||||
sender: response?.extra?.senderAddress,
|
||||
senderPublicKey: response?.extra?.senderPublicKey,
|
||||
reference: response?.callResponse?.reference,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Update transactions in IndexedDB
|
||||
const result = await updateTransactionInDB(transactionData);
|
||||
|
||||
fetchOngoingTransactions()
|
||||
if(isUsingGateway){
|
||||
setRecord(transactionData)
|
||||
await showInfo({
|
||||
message: `Keep a record of your order in case your trade gets stuck`,
|
||||
})
|
||||
}
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'success',
|
||||
message: "Submitted Order"
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: error?.message || "Failed to submit trade order."
|
||||
})
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getRowStyle = (params: RowClassParams<any, any>): RowStyle | undefined => {
|
||||
|
||||
if (listOfOngoingTradesAts.includes(params.data.qortalAtAddress)) {
|
||||
return { background: '#D9D9D91A'};
|
||||
}
|
||||
if (params.data.qortalAtAddress === selectedOffer?.qortalAtAddress) {
|
||||
return { background: '#6D94F533'};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
// const onGridReady = (params) => {
|
||||
// const allColumnIds = params.columnApi.getAllColumns().map(col => col.getColId());
|
||||
// params.columnApi.autoSizeColumns(allColumnIds, false);
|
||||
// };
|
||||
|
||||
const onSelectionChanged = (event: any) => {
|
||||
const selectedRows = event.api.getSelectedRows();
|
||||
|
||||
setSelectedOffers([...selectedRows]); // Set all selected rows
|
||||
};
|
||||
|
||||
const onRowClicked = (event: any) => {
|
||||
if (listOfOngoingTradesAts.includes(event.data.qortalAtAddress)) return;
|
||||
const selectedRows = gridRef.current?.api.getSelectedRows();
|
||||
setSelectedOffers([...selectedRows]); // Always spread the array to ensure state updates correctly
|
||||
};
|
||||
|
||||
|
||||
const updateGridData = (newData: any) => {
|
||||
if (gridRef.current) {
|
||||
|
||||
setOffers(newData);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const getRowId = useCallback(function (params: any) {
|
||||
return String(params.data.qortalAtAddress);
|
||||
}, []);
|
||||
|
||||
const selectedTotalQORT = useMemo(() => {
|
||||
return selectedOffers.reduce((acc: number, curr: any) => {
|
||||
return acc + (+curr.qortAmount || 0); // Ensure qortAmount is defined
|
||||
}, 0);
|
||||
}, [selectedOffers]);
|
||||
|
||||
|
||||
const onGridReady = useCallback((params: any) => {
|
||||
params.api.sizeColumnsToFit(); // Adjust columns to fit the grid width
|
||||
const allColumnIds = params.columnApi.getAllColumns().map((col: any) => col.getColId());
|
||||
params.columnApi.autoSizeColumns(allColumnIds); // Automatically adjust the width to fit content
|
||||
}, []);
|
||||
|
||||
|
||||
const handleClose = (
|
||||
event?: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason,
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setInfo(null)
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
}}>
|
||||
<div className="ag-theme-alpine-dark" style={{ height: 400, width: '100%' }}>
|
||||
<AgGridReact
|
||||
ref={gridRef}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={offersWithoutOngoing}
|
||||
onRowClicked={onRowClicked}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
getRowStyle={getRowStyle}
|
||||
autoSizeStrategy={autoSizeStrategy}
|
||||
rowSelection="multiple" // Enable multi-select
|
||||
rowMultiSelectWithClick={true}
|
||||
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
|
||||
/>
|
||||
{/* {selectedOffer && (
|
||||
<Button onClick={buyOrder}>Buy</Button>
|
||||
|
||||
)} */}
|
||||
</div>
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
position: 'fixed',
|
||||
bottom: '0px',
|
||||
height: '100px',
|
||||
padding: '7px',
|
||||
background: '#181d1f',
|
||||
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
flexDirection: 'column',
|
||||
width: '100%'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: '16px',
|
||||
color: 'white',
|
||||
width: 'calc(100% - 75px)'
|
||||
}}>{selectedTotalQORT?.toFixed(3)} QORT</Typography>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
alignItems: 'center',
|
||||
width: 'calc(100% - 75px)'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: '16px',
|
||||
color: selectedTotalLTC > ltcBalance ? 'red' : 'white',
|
||||
}}><span>{selectedTotalLTC?.toFixed(4)}</span> <span style={{
|
||||
marginLeft: 'auto'
|
||||
}}>LTC</span></Typography>
|
||||
|
||||
|
||||
</Box>
|
||||
<Typography sx={{
|
||||
fontSize: '16px',
|
||||
color: 'white',
|
||||
|
||||
}}><span>{ltcBalance?.toFixed(4)}</span> <span style={{
|
||||
marginLeft: 'auto'
|
||||
}}>LTC balance</span></Typography>
|
||||
</Box>
|
||||
{BuyButton()}
|
||||
</Box>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} onClose={handleClose}>
|
||||
<Alert
|
||||
|
||||
|
||||
onClose={handleClose}
|
||||
severity={info?.type}
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{info?.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
{isShowInfo && (
|
||||
<Dialog
|
||||
open={isShowInfo}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{"Download record"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{messageInfo.message}
|
||||
</DialogContentText>
|
||||
<Button onClick={()=> {
|
||||
saveFileToDisk(record)
|
||||
}}>Save Record</Button>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
<Button variant="contained" onClick={onOkInfo} autoFocus>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
85
src/components/Terms.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export const Terms =() => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button variant="outlined" onClick={handleClickOpen}>
|
||||
Terms and conditions
|
||||
</Button>
|
||||
<BootstrapDialog
|
||||
onClose={handleClose}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title">
|
||||
Terms and Conditions
|
||||
</DialogTitle>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
sx={(theme) => ({
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: theme.palette.grey[500],
|
||||
})}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<DialogContent dividers>
|
||||
<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 use—there are no additional fees for buying QORT through this site. There are two ways to place a buy order:
|
||||
1. Use the gateway
|
||||
2. Use your local node.
|
||||
By using qort.trade, you agree to the following terms and conditions.
|
||||
</Typography>
|
||||
|
||||
<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.
|
||||
</Typography>
|
||||
|
||||
<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.
|
||||
</Typography>
|
||||
|
||||
<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.
|
||||
</Typography>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
13
src/components/common/Spacer.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export const Spacer = ({ height }: any) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: height,
|
||||
display: 'flex',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
15
src/components/common/icons/CaretDownSVG.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CaretDownSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 15 8"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 1L7.5 7L14 1" stroke="#464646" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/components/common/icons/CloseSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CloseSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
onClickFunc,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className={className} onClick={onClickFunc}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
21
src/components/common/icons/DoubleCaretRightSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const DoubleCaretRightSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M383-480 200-664l56-56 240 240-240 240-56-56 183-184Zm264 0L464-664l56-56 240 240-240 240-56-56 183-184Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
24
src/components/common/icons/HomeSVG.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const HomeSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 21 19"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.64729 18.9972C3.27267 18.9972 2.96761 18.685 2.96761 18.2955V10.7916H1.18276H1.18009C0.527159 10.7916 0 10.2446 0 9.57322C0 9.18918 0.173937 8.84659 0.44153 8.6228L2.75086 6.5562V3.11371C2.75086 2.71034 3.06662 2.38432 3.45463 2.38432H4.91569C5.30637 2.38432 5.62213 2.71034 5.62213 3.11371V3.98124L9.72701 0.298386C10.1712 -0.099462 10.8295 -0.099462 11.2737 0.298386L20.594 8.65319C20.9659 8.98749 21.0971 9.52348 20.9258 10.0015C20.7519 10.4767 20.313 10.7944 19.8206 10.7944H18.0331V18.2982C18.0331 18.6878 17.7281 19 17.3534 19H13.3368V13.5406C13.3368 12.7698 12.7321 12.1454 11.9855 12.1454H9.01254C8.26595 12.1454 7.66119 12.7698 7.66119 13.5406V18.9945H3.64729V18.9972Z"
|
||||
fill="#464646"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
7
src/components/common/icons/IconTypes.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface IconTypes {
|
||||
color: string;
|
||||
height: string;
|
||||
width: string;
|
||||
className?: string;
|
||||
onClickFunc?: () => void;
|
||||
}
|
16
src/components/common/icons/InfoSVG.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const InfoSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
<svg onClick={onClickFunc} className={className} width={width} height={height} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 0C5.61553 0 4.26215 0.410543 3.11101 1.17971C1.95987 1.94888 1.06266 3.04213 0.532846 4.32121C0.00303299 5.6003 -0.13559 7.00776 0.134506 8.36563C0.404603 9.7235 1.07129 10.9708 2.05026 11.9497C3.02922 12.9287 4.2765 13.5954 5.63437 13.8655C6.99224 14.1356 8.3997 13.997 9.67879 13.4672C10.9579 12.9373 12.0511 12.0401 12.8203 10.889C13.5895 9.73784 14 8.38447 14 7C13.9979 5.14412 13.2598 3.36484 11.9475 2.05253C10.6352 0.740225 8.85588 0.002064 7 0ZM7 12.6C5.89243 12.6 4.80972 12.2716 3.88881 11.6562C2.96789 11.0409 2.25013 10.1663 1.82628 9.14302C1.40243 8.11976 1.29153 6.99379 1.50761 5.90749C1.72368 4.8212 2.25703 3.82337 3.0402 3.0402C3.82338 2.25703 4.8212 1.72368 5.9075 1.5076C6.99379 1.29153 8.11976 1.40242 9.14303 1.82627C10.1663 2.25012 11.0409 2.96789 11.6562 3.88881C12.2716 4.80972 12.6 5.89242 12.6 7C12.5983 8.48469 12.0078 9.90808 10.9579 10.9579C9.90809 12.0078 8.48469 12.5983 7 12.6ZM7.7 6.3V9.8C7.7 9.98565 7.62625 10.1637 7.49498 10.295C7.3637 10.4262 7.18565 10.5 7 10.5C6.81435 10.5 6.6363 10.4262 6.50503 10.295C6.37375 10.1637 6.3 9.98565 6.3 9.8V7C6.11435 7 5.9363 6.92625 5.80503 6.79497C5.67375 6.6637 5.6 6.48565 5.6 6.3C5.6 6.11435 5.67375 5.9363 5.80503 5.80502C5.9363 5.67375 6.11435 5.6 6.3 5.6H7C7.18565 5.6 7.3637 5.67375 7.49498 5.80502C7.62625 5.9363 7.7 6.11435 7.7 6.3ZM7.7 4.2C7.7 4.33845 7.65895 4.47378 7.58203 4.5889C7.50511 4.70401 7.39579 4.79373 7.26788 4.84671C7.13997 4.8997 6.99923 4.91356 6.86344 4.88655C6.72765 4.85954 6.60292 4.79287 6.50503 4.69497C6.40713 4.59708 6.34046 4.47235 6.31345 4.33656C6.28644 4.20078 6.3003 4.06003 6.35329 3.93212C6.40627 3.80421 6.49599 3.69489 6.6111 3.61797C6.72622 3.54105 6.86156 3.5 7 3.5C7.18565 3.5 7.3637 3.57375 7.49498 3.70502C7.62625 3.8363 7.7 4.01435 7.7 4.2Z" fill={color}/>
|
||||
</svg>
|
||||
|
||||
);
|
||||
};
|
68
src/components/common/icons/QortalLogoSVG.tsx
Normal file
53
src/components/common/icons/QortalLogoWhiteSVG.tsx
Normal file
25
src/components/common/icons/StarSVG.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const StarSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 36 34"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M28.7818 21.7029C28.6179 21.8647 28.542 22.094 28.5799 22.3203L29.8898 31.6695C29.993 32.4158 29.6727 33.1591 29.055 33.6026C28.4388 34.0462 27.6252 34.1211 26.9362 33.7945L18.3259 29.6705C18.1195 29.5716 17.8797 29.5716 17.6732 29.6705L9.06295 33.7945C8.37389 34.1241 7.55731 34.0537 6.93652 33.6086C6.31725 33.1636 5.99701 32.4188 6.10176 31.6695L7.41161 22.3203C7.44955 22.094 7.37366 21.8647 7.20974 21.7029L0.592327 14.9012C0.0520072 14.3573 -0.136207 13.5616 0.100576 12.8378C0.337351 12.1125 0.962693 11.5775 1.72309 11.4486L11.1334 9.79128C11.3626 9.75381 11.5584 9.61145 11.6631 9.40765L16.1711 1.08343H16.1696C16.5308 0.416576 17.2335 0 18 0C18.7665 0 19.4692 0.416576 19.8304 1.08343L24.3384 9.40765H24.3369C24.4416 9.61145 24.6374 9.75381 24.8666 9.79128L34.2769 11.4486C35.0373 11.5775 35.6626 12.1125 35.8994 12.8378C36.1362 13.5616 35.948 14.3573 35.4077 14.9012L28.7818 21.7029Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/components/common/icons/VolumeOffSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const VolumeOffSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Zm-80 238v-94l-72-72H200v80h114l86 86Zm-36-130Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/components/common/icons/VolumeOnSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const VolumeOnSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
53
src/components/common/notification/Notification.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import { Zoom } from "react-toastify";
|
||||
import { NotificationContext } from "../../../contexts/notificationContext";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
export const Notification = () => {
|
||||
|
||||
const { notification } = useContext(NotificationContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (notification?.alertType === "alertError") {
|
||||
toast.error(`❌ ${notification?.msg}`, {
|
||||
position: "bottom-right",
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
toastId: "error",
|
||||
});
|
||||
}
|
||||
if (notification?.alertType === "alertSuccess") {
|
||||
toast.success(`✔️ ${notification?.msg}`, {
|
||||
position: "bottom-right",
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
toastId: "success",
|
||||
});
|
||||
}
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ToastContainer
|
||||
transition={Zoom}
|
||||
position="bottom-right"
|
||||
autoClose={false}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,53 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
export const ReusableModalContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "629px",
|
||||
minHeight: "446px",
|
||||
maxWidth: '90vw',
|
||||
height: "auto",
|
||||
borderRadius: "20px",
|
||||
border: "20px solid #3F3F3F",
|
||||
zIndex: "100",
|
||||
boxShadow:
|
||||
"0px 4px 5px 0px hsla(0,0%,0%,0.14), \n\t\t0px 1px 10px 0px hsla(0,0%,0%,0.12), \n\t\t0px 2px 4px -1px hsla(0,0%,0%,0.2)",
|
||||
}));
|
||||
|
||||
export const ReusableModalSubContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "20px",
|
||||
padding: "70px",
|
||||
});
|
||||
|
||||
export const ReusableModalBackdrop = styled(Box)({
|
||||
position: "fixed",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
backdropFilter: "blur(1px)",
|
||||
zIndex: "99",
|
||||
});
|
||||
|
||||
export const ReusableModalButton = styled(Button)(({ theme }) => ({
|
||||
width: "auto",
|
||||
height: "43px",
|
||||
padding: "10px 20px 10px 20px",
|
||||
gap: "10px",
|
||||
borderRadius: "30px",
|
||||
border: `1px solid ${theme.palette.text.primary}`,
|
||||
color: theme.palette.text.primary,
|
||||
boxShadow: "1px 4px 10.5px 0px #0000004D"
|
||||
}));
|
20
src/components/common/reusable-modal/ReusableModal.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import {
|
||||
ReusableModalBackdrop,
|
||||
ReusableModalContainer,
|
||||
ReusableModalSubContainer,
|
||||
} from "./ReusableModal-styles";
|
||||
interface ReusableModalProps {
|
||||
backdrop?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ReusableModal: React.FC<ReusableModalProps> = ({ backdrop, children }) => {
|
||||
return (
|
||||
<>
|
||||
<ReusableModalContainer>
|
||||
<ReusableModalSubContainer>{children}</ReusableModalSubContainer>
|
||||
</ReusableModalContainer>
|
||||
{backdrop && <ReusableModalBackdrop />}
|
||||
</>
|
||||
);
|
||||
};
|
64
src/components/common/useModal.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
interface State {
|
||||
isShow: boolean;
|
||||
}
|
||||
export const useModal = () => {
|
||||
const [state, setState] = useState<State>({
|
||||
isShow: false,
|
||||
});
|
||||
const [message, setMessage] = useState({
|
||||
publishFee: "",
|
||||
message: ""
|
||||
});
|
||||
const promiseConfig = useRef<any>(null);
|
||||
const show = async (data) => {
|
||||
setMessage(data)
|
||||
return new Promise((resolve, reject) => {
|
||||
promiseConfig.current = {
|
||||
resolve,
|
||||
reject,
|
||||
};
|
||||
setState({
|
||||
isShow: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
setState({
|
||||
isShow: false,
|
||||
});
|
||||
setMessage({
|
||||
publishFee: "",
|
||||
message: ""
|
||||
})
|
||||
};
|
||||
|
||||
const onOk = (payload:any) => {
|
||||
const { resolve } = promiseConfig.current;
|
||||
setMessage({
|
||||
publishFee: "",
|
||||
message: ""
|
||||
})
|
||||
hide();
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
const { reject } = promiseConfig.current;
|
||||
hide();
|
||||
reject();
|
||||
setMessage({
|
||||
publishFee: "",
|
||||
message: ""
|
||||
})
|
||||
};
|
||||
return {
|
||||
show,
|
||||
onOk,
|
||||
onCancel,
|
||||
isShow: state.isShow,
|
||||
message
|
||||
};
|
||||
};
|
64
src/components/game/Game-styles.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
interface ICellProps {
|
||||
borderTopStyle?: boolean;
|
||||
borderRightStyle?: boolean;
|
||||
borderLeftStyle?: boolean;
|
||||
borderBottomStyle?: boolean;
|
||||
}
|
||||
|
||||
export const Cell = styled(Box)<ICellProps>(
|
||||
({
|
||||
borderTopStyle,
|
||||
borderLeftStyle,
|
||||
borderBottomStyle,
|
||||
borderRightStyle,
|
||||
}) => ({
|
||||
width: "13em",
|
||||
height: "9em",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "20px",
|
||||
cursor: "pointer",
|
||||
borderTop: borderTopStyle ? "3px solid #8e44ad" : "none",
|
||||
borderLeft: borderLeftStyle ? "3px solid #8e44ad" : "none",
|
||||
borderBottom: borderBottomStyle ? "3px solid #8e44ad" : "none",
|
||||
borderRight: borderRightStyle ? "3px solid #8e44ad" : "none",
|
||||
transition: "all 270ms ease-in-out",
|
||||
"&:hover": {
|
||||
backgroundColor: "#8d44ad28",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const X = styled("span")({
|
||||
fontSize: "100px",
|
||||
color: "#8e44ad",
|
||||
"&::after": {
|
||||
content: "X",
|
||||
},
|
||||
});
|
||||
|
||||
export const O = styled("span")({
|
||||
fontSize: "100px",
|
||||
color: "#8e44ad",
|
||||
"&::after": {
|
||||
content: "O",
|
||||
},
|
||||
});
|
||||
|
||||
export const GameContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "100px 0 50px 0",
|
||||
gap: "15px",
|
||||
fontFamily: "Zen Tokyo Zoo, cursive",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const RowContainer = styled(Box)({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
});
|
156
src/components/header/Header-styles.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { HomeSVG } from "../common/icons/HomeSVG";
|
||||
import { QortalLogoSVG } from "../common/icons/QortalLogoSVG";
|
||||
import { CaretDownSVG } from "../common/icons/CaretDownSVG";
|
||||
|
||||
export const HeaderNav = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
padding: "0 30px",
|
||||
[theme.breakpoints.only("xs")]: {
|
||||
padding: "0",
|
||||
},
|
||||
}));
|
||||
|
||||
export const HomeIcon = styled(HomeSVG)({
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const QortalLogoIcon = styled(QortalLogoSVG)({
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const CaretDownIcon = styled(CaretDownSVG)({
|
||||
color: "none",
|
||||
});
|
||||
|
||||
export const DropdownContainer = styled(Box)({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "18px",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const GameSelectDropdown = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "12px",
|
||||
width: "fit-content",
|
||||
height: "35px",
|
||||
top: "38px",
|
||||
left: "38px",
|
||||
opacity: "0px",
|
||||
fontFamily: "Fira Sans, sans-serif",
|
||||
fontWeight: 400,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19.2px",
|
||||
border: `1px solid ${theme.palette.text.secondary}`,
|
||||
color: theme.palette.text.secondary,
|
||||
borderRadius: "30px",
|
||||
gap: "11px",
|
||||
userSelect: "none",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
border: `1px solid #5f5e5e`,
|
||||
color: "#5f5e5e",
|
||||
"& ${CaretDownIcon}": {
|
||||
"& path": {
|
||||
transition: "all 0.3s ease-in-out",
|
||||
stroke: "#5f5e5e",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const GameSelectDropdownMenu = styled(Box)({
|
||||
position: "absolute",
|
||||
bottom: "-60px",
|
||||
left: 0,
|
||||
backgroundColor: "#222222",
|
||||
border: "1.07px solid #0000001A",
|
||||
borderRadius: "5px",
|
||||
width: "250px",
|
||||
height: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0px 4.27px 14.93px 0px #00000026",
|
||||
"& :first-child": {
|
||||
borderTopLeftRadius: "5px",
|
||||
borderTopRightRadius: "5px",
|
||||
},
|
||||
"& :last-child": {
|
||||
borderBottomLeftRadius: "5px",
|
||||
borderBottomRightRadius: "5px",
|
||||
},
|
||||
});
|
||||
|
||||
export const GameSelectDropdownMenuItem = styled(Box)(({ theme }) => ({
|
||||
fontFamily: "Inter, sans-serif",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "18px",
|
||||
lineHeight: "19.36px",
|
||||
fontWeight: 400,
|
||||
height: "50px",
|
||||
width: "100%",
|
||||
padding: "15px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
cursor: "pointer",
|
||||
},
|
||||
}));
|
||||
|
||||
export const Username = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fira Sans, sans-serif",
|
||||
fontSize: "16px",
|
||||
lineHeight: "19.2px",
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
transition: "all 0.3s ease-in-out",
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const NameRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
});
|
||||
export const LogoColumn = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
});
|
||||
export const RightColumn = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
alignItems: "flex-start",
|
||||
padding: '10px'
|
||||
});
|
||||
export const AvatarCircle = styled("img")({
|
||||
borderRadius: "50%",
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
objectFit: "cover",
|
||||
userSelect: "none",
|
||||
});
|
||||
|
||||
|
||||
export const HeaderText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Inter",
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 500,
|
||||
fontSize: "16px",
|
||||
lineHeight: 1.2,
|
||||
userSelect: "none",
|
||||
}));
|
269
src/components/header/Header.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
import { useState, useEffect, useRef, useContext, ChangeEvent } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import {
|
||||
AvatarCircle,
|
||||
CaretDownIcon,
|
||||
DropdownContainer,
|
||||
GameSelectDropdown,
|
||||
GameSelectDropdownMenu,
|
||||
GameSelectDropdownMenuItem,
|
||||
HeaderNav,
|
||||
HeaderText,
|
||||
HomeIcon,
|
||||
LogoColumn,
|
||||
NameRow,
|
||||
QortalLogoIcon,
|
||||
RightColumn,
|
||||
Username,
|
||||
} from "./Header-styles";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
import { UserContext } from "../../contexts/userContext";
|
||||
import { cropAddress } from "../../utils/cropAddress";
|
||||
import { BubbleCardColored1 } from "../../pages/Home/Home-Styles";
|
||||
import logoSVG from "../../assets/SVG/LOGO.svg";
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Snackbar,
|
||||
SnackbarCloseReason,
|
||||
Switch,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { sendRequestToExtension } from "../../App";
|
||||
import { Terms } from "../Terms";
|
||||
|
||||
const checkIfLocal = async () => {
|
||||
try {
|
||||
const response = await sendRequestToExtension("CHECK_IF_LOCAL");
|
||||
|
||||
if (!response.error) {
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const Label = styled("label")(
|
||||
({ theme }) => `
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 400;
|
||||
`
|
||||
);
|
||||
|
||||
export const Header = ({ qortBalance, ltcBalance, mode, setMode }: any) => {
|
||||
const [openDropdown, setOpenDropdown] = useState<boolean>(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [info, setInfo] = useState<any>(null);
|
||||
const { isUsingGateway } = useContext(gameContext);
|
||||
const [selectedCoin, setSelectedCoin] = useState("LITECOIN");
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setChecked(false);
|
||||
setOpen(true);
|
||||
setInfo({
|
||||
type: "error",
|
||||
message: "Change the node you are using at the authentication page",
|
||||
});
|
||||
};
|
||||
const { userInfo } = useContext(gameContext);
|
||||
const { avatar, setAvatar } = useContext(UserContext);
|
||||
|
||||
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
|
||||
padding: 8,
|
||||
"& .MuiSwitch-track": {
|
||||
borderRadius: 22 / 2,
|
||||
"&::before, &::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
"&::before": {
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
|
||||
theme.palette.getContrastText(theme.palette.primary.main)
|
||||
)}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
|
||||
left: 12,
|
||||
},
|
||||
"&::after": {
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
|
||||
theme.palette.getContrastText(theme.palette.primary.main)
|
||||
)}" d="M19,13H5V11H19V13Z" /></svg>')`,
|
||||
right: 12,
|
||||
},
|
||||
},
|
||||
"& .MuiSwitch-thumb": {
|
||||
boxShadow: "none",
|
||||
width: 16,
|
||||
height: 16,
|
||||
margin: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
const handleClose = (
|
||||
event?: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason
|
||||
) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setInfo(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpenDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// // Fetch avatar on userInfo change
|
||||
// useEffect(() => {
|
||||
// if (userInfo?.name) {
|
||||
// getAvatar();
|
||||
// }
|
||||
// }, [userInfo]);
|
||||
|
||||
return (
|
||||
<HeaderNav
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{userInfo?.name ? (
|
||||
<Username>{userInfo?.name}</Username>
|
||||
) : userInfo?.address ? (
|
||||
<Username>{cropAddress(userInfo?.address)}</Username>
|
||||
) : null}
|
||||
|
||||
{userInfo?.name ? (
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
fontSize: "15px",
|
||||
}}
|
||||
src={`/arbitrary/THUMBNAIL/${userInfo?.name}/qortal_avatar?encoding=base64&rebuild=false`}
|
||||
alt={`${userInfo?.name}`}
|
||||
>
|
||||
{userInfo?.name?.charAt(0)?.toUpperCase()}
|
||||
</Avatar>
|
||||
) : userInfo?.address ? (
|
||||
<BubbleCardColored1 style={{ height: "35px", width: "35px" }} />
|
||||
) : (
|
||||
<QortalLogoIcon
|
||||
height="35"
|
||||
width="35"
|
||||
color="none"
|
||||
onClickFunc={() => {
|
||||
window.open("https://www.qortal.dev", "_blank")?.focus();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NameRow>
|
||||
</RightColumn>
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
control={
|
||||
<LocalNodeSwitch checked={isUsingGateway} onChange={handleChange} />
|
||||
}
|
||||
label="Is using Gateway"
|
||||
/>
|
||||
<Terms />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => setMode("buy")}>Buy QORT</Button>
|
||||
<Button onClick={() => setMode("sell")}>SELL QORT</Button>
|
||||
</Box>
|
||||
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
open={open}
|
||||
autoHideDuration={6000}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleClose}
|
||||
severity={info?.type}
|
||||
variant="filled"
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{info?.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</HeaderNav>
|
||||
);
|
||||
};
|
258
src/components/sell/CreateSell.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
import { Alert, Box, Button, DialogActions, DialogContent, DialogTitle, IconButton, InputLabel, Snackbar, SnackbarCloseReason, TextField, Typography, styled } from '@mui/material'
|
||||
import React, { useContext } from 'react'
|
||||
import { BootstrapDialog } from '../Terms'
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { Spacer } from '../common/Spacer';
|
||||
import gameContext from '../../contexts/gameContext';
|
||||
import TradeBotList from './TradeBotList';
|
||||
|
||||
export const CustomLabel = styled(InputLabel)`
|
||||
font-weight: 400;
|
||||
font-family: Inter;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
|
||||
`
|
||||
|
||||
export const minimumAmountSellTrades = {
|
||||
'LITECOIN': {
|
||||
value: 0.01,
|
||||
ticker: 'LTC'
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomInput = styled(TextField)({
|
||||
width: "183px", // Adjust the width as needed
|
||||
borderRadius: "5px",
|
||||
// backgroundColor: "rgba(30, 30, 32, 1)",
|
||||
outline: "none",
|
||||
input: {
|
||||
fontSize: 10,
|
||||
fontFamily: "Inter",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
"&::placeholder": {
|
||||
fontSize: 16,
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
outline: "none",
|
||||
padding: "10px",
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
"& fieldset": {
|
||||
border: '0.5px solid rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
"&:hover fieldset": {
|
||||
border: '0.5px solid rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
border: '0.5px solid rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
},
|
||||
"& .MuiInput-underline:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"& .MuiInput-underline:hover:not(.Mui-disabled):before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"& .MuiInput-underline:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const CreateSell = ({qortAddress, show}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [qortAmount, setQortAmount] = React.useState(0)
|
||||
const [foreignAmount, setForeignAmount] = React.useState(0)
|
||||
const {updateTemporaryFailedTradeBots, sellOrders, fetchTemporarySellOrders, isUsingGateway} = useContext(gameContext)
|
||||
const [openAlert, setOpenAlert] = React.useState(false)
|
||||
const [info, setInfo] = React.useState<any>(null)
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
setForeignAmount(0)
|
||||
setQortAmount(0)
|
||||
};
|
||||
|
||||
const createSellOrder = async() => {
|
||||
try {
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'info',
|
||||
message: "Attempting to create sell order. Please wait..."
|
||||
})
|
||||
const res = await qortalRequestWithTimeout({
|
||||
action: "CREATE_TRADE_SELL_ORDER",
|
||||
qortAmount,
|
||||
foreignBlockchain: 'LITECOIN',
|
||||
foreignAmount
|
||||
}, 900000);
|
||||
|
||||
if(res?.error && res?.failedTradeBot){
|
||||
await updateTemporaryFailedTradeBots({
|
||||
atAddress: res?.failedTradeBot?.atAddress,
|
||||
status: 'FAILED',
|
||||
qortAddress: res?.failedTradeBot?.creatorAddress,
|
||||
|
||||
})
|
||||
fetchTemporarySellOrders()
|
||||
setOpenAlert(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: "Unable to create sell order. Please try again."
|
||||
})
|
||||
}
|
||||
if(!res?.error){
|
||||
setOpenAlert(true)
|
||||
setForeignAmount(0)
|
||||
setQortAmount(0)
|
||||
setOpen(false)
|
||||
|
||||
setInfo({
|
||||
type: 'success',
|
||||
message: "Sell order created. Please wait a couple of minutes for the network to propogate the changes."
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if(error?.error && error?.failedTradeBot){
|
||||
await updateTemporaryFailedTradeBots({
|
||||
atAddress: error?.failedTradeBot?.atAddress,
|
||||
status: 'FAILED',
|
||||
qortAddress: error?.failedTradeBot?.creatorAddress,
|
||||
|
||||
})
|
||||
fetchTemporarySellOrders()
|
||||
setOpenAlert(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: "Unable to create sell order. Please try again."
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseAlert = (
|
||||
event?: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason,
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenAlert(false);
|
||||
setInfo(null)
|
||||
};
|
||||
|
||||
if(isUsingGateway){
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
display: show ? 'flex' : 'none',
|
||||
height: '500px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<Typography sx={{
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
display: show ? 'block' : 'none'
|
||||
}}>
|
||||
<Button onClick={handleClickOpen}>New Sell Order</Button>
|
||||
<TradeBotList qortAddress={qortAddress} failedTradeBots={sellOrders.filter((item)=> item.status === 'FAILED')} />
|
||||
|
||||
<BootstrapDialog
|
||||
onClose={handleClose}
|
||||
aria-labelledby="customized-dialog-title"
|
||||
open={open}
|
||||
sx={{
|
||||
'& .MuiDialogContent-root': {
|
||||
width: '300px'
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title">
|
||||
New Sell Order - QORT for LTC
|
||||
</DialogTitle>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
sx={(theme) => ({
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: theme.palette.grey[500],
|
||||
})}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<DialogContent dividers>
|
||||
<Box>
|
||||
<CustomLabel htmlFor="standard-adornment-name">QORT amount</CustomLabel>
|
||||
<Spacer height="5px" />
|
||||
<CustomInput
|
||||
id="standard-adornment-name"
|
||||
type="number"
|
||||
value={qortAmount}
|
||||
onChange={(e) => setQortAmount(+e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Spacer height="6px" />
|
||||
<CustomLabel htmlFor="standard-adornment-amount">
|
||||
Price Each (LTC)
|
||||
</CustomLabel>
|
||||
<Spacer height="5px" />
|
||||
<CustomInput
|
||||
id="standard-adornment-amount"
|
||||
type="number"
|
||||
value={foreignAmount}
|
||||
onChange={(e) => setForeignAmount(+e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Spacer height="6px" />
|
||||
<Typography>{qortAmount * foreignAmount} LTC for {qortAmount} QORT</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: '12px'
|
||||
}}>Total sell amount needs to be greater than: {minimumAmountSellTrades.LITECOIN.value} {' '} {minimumAmountSellTrades.LITECOIN.ticker}</Typography>
|
||||
</Box>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button disabled={!qortAmount || !(qortAmount * foreignAmount > minimumAmountSellTrades.LITECOIN.value)} autoFocus onClick={createSellOrder}>
|
||||
Create sell order
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={openAlert} onClose={handleCloseAlert}>
|
||||
<Alert
|
||||
|
||||
|
||||
onClose={handleCloseAlert}
|
||||
severity={info?.type}
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{info?.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</div>
|
||||
)
|
||||
}
|
363
src/components/sell/TradeBotList.tsx
Normal file
@ -0,0 +1,363 @@
|
||||
import { ColDef } from "ag-grid-community";
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { autoSizeStrategy } from "../Grids/TradeOffers";
|
||||
import { Alert, Box, Snackbar, SnackbarCloseReason, Typography } from "@mui/material";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
|
||||
const defaultColDef = {
|
||||
resizable: true, // Make columns resizable by default
|
||||
sortable: true, // Make columns sortable by default
|
||||
suppressMovable: true, // Prevent columns from being movable
|
||||
};
|
||||
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
headerCheckboxSelection: false, // Adds a checkbox in the header for selecting all rows
|
||||
checkboxSelection: true, // Adds checkboxes in each row for selection
|
||||
headerName: "Select", // You can customize the header name
|
||||
width: 50, // Adjust the width as needed
|
||||
pinned: "left", // Optional, to pin this column on the left
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
headerName: "QORT AMOUNT",
|
||||
field: "qortAmount",
|
||||
flex: 1, // Flex makes this column responsive
|
||||
minWidth: 150, // Ensure it doesn't shrink too much
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
headerName: "LTC/QORT",
|
||||
valueGetter: (params) =>
|
||||
+params.data.foreignAmount / +params.data.qortAmount,
|
||||
sortable: true,
|
||||
sort: "asc",
|
||||
flex: 1, // Flex makes this column responsive
|
||||
minWidth: 150, // Ensure it doesn't shrink too much
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
headerName: "Total LTC Value",
|
||||
field: "foreignAmount",
|
||||
flex: 1, // Flex makes this column responsive
|
||||
minWidth: 150, // Ensure it doesn't shrink too much
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
headerName: "Status",
|
||||
field: "status",
|
||||
flex: 1, // Flex makes this column responsive
|
||||
minWidth: 300, // Ensure it doesn't shrink too much
|
||||
resizable: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function TradeBotList({ qortAddress, failedTradeBots }) {
|
||||
const [tradeBotList, setTradeBotList] = useState([]);
|
||||
const [selectedTrade, setSelectedTrade] = useState(null);
|
||||
const tradeBotListRef = useRef([])
|
||||
const 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(() => {
|
||||
if (qortAddress) {
|
||||
qortAddressRef.current = qortAddress;
|
||||
}
|
||||
}, [qortAddress]);
|
||||
|
||||
const restartTradeOffersWebSocket = () => {
|
||||
setTimeout(() => initTradeOffersWebSocket(true), 50);
|
||||
};
|
||||
|
||||
const processTradeBotState = (state) => {
|
||||
if (state.creatorAddress === qortAddressRef.current) {
|
||||
switch (state.tradeState) {
|
||||
case "BOB_WAITING_FOR_AT_CONFIRM":
|
||||
return "PENDING";
|
||||
|
||||
case "BOB_WAITING_FOR_MESSAGE":
|
||||
return "LISTED";
|
||||
|
||||
case "BOB_WAITING_FOR_AT_REDEEM":
|
||||
return "TRADING";
|
||||
|
||||
case "BOB_DONE":
|
||||
case "BOB_REFUNDED":
|
||||
case "ALICE_DONE":
|
||||
case "ALICE_REFUNDED":
|
||||
return null;
|
||||
|
||||
case "ALICE_WAITING_FOR_AT_LOCK":
|
||||
return "BUYING";
|
||||
|
||||
case "ALICE_REFUNDING_A":
|
||||
return "REFUNDING";
|
||||
|
||||
default:
|
||||
return null; // Return null or a default value if no tradeState matches
|
||||
}
|
||||
}
|
||||
return null; // Return null if creatorAddress doesn't match qortAddressRef.current
|
||||
};
|
||||
|
||||
const processTradeBots = (tradeBots) => {
|
||||
let sellTrades = [...tradeBotListRef.current]; // Start with the existing trades
|
||||
|
||||
tradeBots.forEach((trade) => {
|
||||
const status = processTradeBotState(trade);
|
||||
|
||||
if (status) {
|
||||
// Check if the trade is already in the list
|
||||
const existingIndex = sellTrades.findIndex(
|
||||
(existingTrade) => existingTrade.atAddress === trade.atAddress
|
||||
);
|
||||
|
||||
if (existingIndex > -1) {
|
||||
// Replace the existing trade if it exists
|
||||
sellTrades[existingIndex] = { ...trade, status };
|
||||
} else {
|
||||
// Add new trade if it doesn't exist
|
||||
sellTrades.push({ ...trade, status });
|
||||
}
|
||||
}
|
||||
});
|
||||
setTradeBotList(sellTrades);
|
||||
tradeBotListRef.current = sellTrades;
|
||||
};
|
||||
|
||||
const initTradeOffersWebSocket = (restarted = false) => {
|
||||
let tradeOffersSocketCounter = 0;
|
||||
let socketTimeout: any;
|
||||
// let socketLink = `ws://127.0.0.1:12391/websockets/crosschain/tradebot?foreignBlockchain=LITECOIN`;
|
||||
let socketLink = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/websockets/crosschain/tradebot?foreignBlockchain=LITECOIN`;
|
||||
const socket = new WebSocket(socketLink);
|
||||
socket.onopen = () => {
|
||||
setTimeout(pingSocket, 50);
|
||||
tradeOffersSocketCounter += 1;
|
||||
};
|
||||
socket.onmessage = (e) => {
|
||||
tradeOffersSocketCounter += 1;
|
||||
restarted = false;
|
||||
processTradeBots(JSON.parse(e.data));
|
||||
};
|
||||
socket.onclose = () => {
|
||||
clearTimeout(socketTimeout);
|
||||
restartTradeOffersWebSocket();
|
||||
};
|
||||
socket.onerror = (e) => {
|
||||
clearTimeout(socketTimeout);
|
||||
};
|
||||
const pingSocket = () => {
|
||||
socket.send("ping");
|
||||
socketTimeout = setTimeout(pingSocket, 295000);
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(!qortAddress) return
|
||||
initTradeOffersWebSocket();
|
||||
}, [qortAddress]);
|
||||
|
||||
const onSelectionChanged = (event: any) => {
|
||||
const selectedRows = event.api.getSelectedRows();
|
||||
if(selectedRows[0]){
|
||||
setSelectedTrade(selectedRows[0])
|
||||
} else {
|
||||
setSelectedTrade(null)
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (
|
||||
event?: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason,
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setInfo(null)
|
||||
};
|
||||
|
||||
|
||||
|
||||
const cancelSell = async ()=> {
|
||||
try {
|
||||
if(!selectedTrade) return
|
||||
setOpen(true)
|
||||
|
||||
setInfo({
|
||||
type: 'info',
|
||||
message: "Attempting to cancel sell order"
|
||||
})
|
||||
const res = await qortalRequestWithTimeout({
|
||||
action: "CANCEL_TRADE_SELL_ORDER",
|
||||
qortAmount: selectedTrade.qortAmount,
|
||||
foreignBlockchain: 'LITECOIN',
|
||||
foreignAmount: selectedTrade.foreignAmount,
|
||||
atAddress: selectedTrade.atAddress
|
||||
}, 900000);
|
||||
if(res?.signature){
|
||||
await deleteTemporarySellOrder(selectedTrade.atAddress)
|
||||
|
||||
|
||||
setSelectedTrade(null)
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'success',
|
||||
message: "Sell order canceled. Please wait a couple of minutes for the network to propogate the changes"
|
||||
})
|
||||
}
|
||||
if(res?.error && res?.failedTradeBot){
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: "Unable to cancel sell order. Please try again."
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if(error?.error && error?.failedTradeBot){
|
||||
setOpen(true)
|
||||
setInfo({
|
||||
type: 'error',
|
||||
message: "Unable to cancel sell order. Please try again."
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CancelButton = () => {
|
||||
return (
|
||||
<button disabled={!selectedTrade || selectedTrade?.status === 'PENDING'} onClick={cancelSell} style={{borderRadius: '8px', width: '150px', height:"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'
|
||||
}}>
|
||||
Cancel sell order
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="ag-theme-alpine-dark"
|
||||
style={{ height: 400, width: "100%" }}
|
||||
>
|
||||
<AgGridReact
|
||||
ref={gridRef}
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={filteredOutTradeBotListWithoutFailed}
|
||||
// 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
|
||||
/>
|
||||
{/* {selectedOffer && (
|
||||
<Button onClick={buyOrder}>Buy</Button>
|
||||
|
||||
)} */}
|
||||
</div>
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
position: 'fixed',
|
||||
bottom: '0px',
|
||||
height: '100px',
|
||||
padding: '7px',
|
||||
background: '#181d1f',
|
||||
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
flexDirection: 'column',
|
||||
width: '100%'
|
||||
}}>
|
||||
{/* <Typography sx={{
|
||||
fontSize: '16px',
|
||||
color: 'white',
|
||||
width: 'calc(100% - 75px)'
|
||||
}}>{selectedTotalQORT?.toFixed(3)} QORT</Typography> */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
alignItems: 'center',
|
||||
width: 'calc(100% - 75px)'
|
||||
}}>
|
||||
{/* <Typography sx={{
|
||||
fontSize: '16px',
|
||||
color: selectedTotalLTC > ltcBalance ? 'red' : 'white',
|
||||
}}><span>{selectedTotalLTC?.toFixed(4)}</span> <span style={{
|
||||
marginLeft: 'auto'
|
||||
}}>LTC</span></Typography> */}
|
||||
|
||||
|
||||
</Box>
|
||||
{/* <Typography sx={{
|
||||
fontSize: '16px',
|
||||
color: 'white',
|
||||
|
||||
}}><span>{ltcBalance?.toFixed(4)}</span> <span style={{
|
||||
marginLeft: 'auto'
|
||||
}}>LTC balance</span></Typography> */}
|
||||
</Box>
|
||||
{CancelButton()}
|
||||
</Box>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} onClose={handleClose}>
|
||||
<Alert
|
||||
|
||||
|
||||
onClose={handleClose}
|
||||
severity={info?.type}
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{info?.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</div>
|
||||
);
|
||||
}
|
2
src/constants/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const playingAmount = 0.25;
|
||||
export const homeAddress = 'Qa4cxK73TXWgA8fXs7sK1fp96syCQTSTmQ'
|
58
src/contexts/gameContext.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { NULL } from "sass";
|
||||
|
||||
|
||||
export interface User {
|
||||
_id: string;
|
||||
qortAddress: string;
|
||||
}
|
||||
|
||||
|
||||
export interface UserNameAvatar {
|
||||
name: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface IContextProps {
|
||||
ltcBalance: number | null;
|
||||
qortBalance: number | null;
|
||||
userInfo: any;
|
||||
setUserInfo: (val: any) => void;
|
||||
userNameAvatar: Record<string, UserNameAvatar>;
|
||||
setUserNameAvatar: (userNameAvatar: Record<string, UserNameAvatar>) => void;
|
||||
onGoingTrades: any[];
|
||||
fetchOngoingTransactions: ()=> void
|
||||
isAuthenticated: boolean;
|
||||
setIsAuthenticated: (val: any)=> void;
|
||||
OAuthLoading: boolean;
|
||||
setOAuthLoading: (val: boolean)=> void;
|
||||
updateTransactionInDB:(val: any)=> void;
|
||||
sellOrders: any[];
|
||||
deleteTemporarySellOrder: (val: any)=> void;
|
||||
updateTemporaryFailedTradeBots: (val: any)=> void;
|
||||
fetchTemporarySellOrders: ()=> void;
|
||||
isUsingGateway: boolean;
|
||||
}
|
||||
|
||||
const defaultState: IContextProps = {
|
||||
qortBalance: null,
|
||||
ltcBalance: null,
|
||||
userInfo: null,
|
||||
setUserInfo: () => {},
|
||||
userNameAvatar: {},
|
||||
setUserNameAvatar: () => {},
|
||||
onGoingTrades: [],
|
||||
fetchOngoingTransactions: ()=> {},
|
||||
isAuthenticated: false,
|
||||
setIsAuthenticated: ()=> {},
|
||||
OAuthLoading: false,
|
||||
setOAuthLoading: ()=> {},
|
||||
updateTransactionInDB:()=> {},
|
||||
sellOrders: [],
|
||||
deleteTemporarySellOrder: ()=> {},
|
||||
updateTemporaryFailedTradeBots: ()=> {},
|
||||
fetchTemporarySellOrders: ()=> {},
|
||||
isUsingGateway: true
|
||||
};
|
||||
|
||||
export default React.createContext(defaultState);
|
19
src/contexts/indexedDBContext.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
import React, { createContext, useContext } from "react";
|
||||
import useIndexedDB from "../hooks/useIndexedDB";
|
||||
|
||||
const IndexedDBContext = createContext(null);
|
||||
|
||||
export const IndexedDBProvider = ({ children }) => {
|
||||
const db = useIndexedDB();
|
||||
|
||||
return (
|
||||
<IndexedDBContext.Provider value={db}>
|
||||
{children}
|
||||
</IndexedDBContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useIndexedDBContext = () => {
|
||||
return useContext(IndexedDBContext);
|
||||
};
|
11
src/contexts/loadingContext.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface LoadingContextProps {
|
||||
loadingSlider: boolean;
|
||||
setLoadingSlider: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const LoadingContext = createContext<LoadingContextProps>({
|
||||
loadingSlider: false,
|
||||
setLoadingSlider: () => {},
|
||||
});
|
24
src/contexts/notificationContext.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface AlertTypes {
|
||||
alertSuccess: string;
|
||||
alertError: string;
|
||||
alertInfo: string;
|
||||
}
|
||||
|
||||
export interface NotificationProps {
|
||||
alertType: keyof AlertTypes | '';
|
||||
msg: string;
|
||||
}
|
||||
|
||||
interface NotificationContextProps {
|
||||
notification: NotificationProps;
|
||||
setNotification: (notification: NotificationProps) => void;
|
||||
resetNotification: () => void;
|
||||
}
|
||||
|
||||
export const NotificationContext = createContext<NotificationContextProps>({
|
||||
notification: { alertType: '', msg: ''},
|
||||
setNotification: () => {},
|
||||
resetNotification: () => {},
|
||||
});
|
13
src/contexts/userContext.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface UserContextProps {
|
||||
avatar: string;
|
||||
setAvatar: (avatar: string) => void;
|
||||
}
|
||||
|
||||
export const UserContext = createContext<UserContextProps>({
|
||||
avatar: '',
|
||||
setAvatar: () => {},
|
||||
});
|
||||
|
||||
|
67
src/global.d.ts
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
// src/global.d.ts
|
||||
interface QortalRequestOptions {
|
||||
action: string;
|
||||
name?: string;
|
||||
service?: string;
|
||||
data64?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
identifier?: string;
|
||||
address?: string;
|
||||
metaData?: string;
|
||||
encoding?: string;
|
||||
includeMetadata?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
reverse?: boolean;
|
||||
resources?: any[];
|
||||
filename?: string;
|
||||
list_name?: string;
|
||||
item?: string;
|
||||
items?: strings[];
|
||||
tag1?: string;
|
||||
tag2?: string;
|
||||
tag3?: string;
|
||||
tag4?: string;
|
||||
tag5?: string;
|
||||
coin?: string;
|
||||
destinationAddress?: string;
|
||||
amount?: number;
|
||||
blob?: Blob;
|
||||
mimeType?: string;
|
||||
file?: File;
|
||||
encryptedData?: string;
|
||||
mode?: string;
|
||||
query?: string;
|
||||
excludeBlocked?: boolean;
|
||||
exactMatchNames?: boolean;
|
||||
nameListFilter?: string[];
|
||||
crosschainAtInfo?: any[];
|
||||
qortAmount?: number;
|
||||
foreignBlockchain?: string;
|
||||
foreignAmount?: number;
|
||||
atAddress?: string;
|
||||
}
|
||||
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
|
||||
declare function qortalRequestWithTimeout(
|
||||
options: QortalRequestOptions,
|
||||
time: number
|
||||
): Promise<any>;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_qdnBase: any; // Replace 'any' with the appropriate type if you know it
|
||||
_qdnTheme: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
showSaveFilePicker: (
|
||||
options?: SaveFilePickerOptions
|
||||
) => Promise<FileSystemFileHandle>;
|
||||
}
|
||||
}
|
24
src/hooks/useIndexedDB.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
// src/hooks/useIndexedDB.js
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { openDatabase } from "../utils/indexedDB";
|
||||
|
||||
const useIndexedDB = () => {
|
||||
const [db, setDb] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeDB = async () => {
|
||||
try {
|
||||
const database = await openDatabase();
|
||||
setDb(database);
|
||||
} catch (error) {
|
||||
console.error("Failed to open IndexedDB:", error);
|
||||
}
|
||||
};
|
||||
initializeDB();
|
||||
}, []);
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
export default useIndexedDB;
|
48
src/index.scss
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
@font-face {
|
||||
font-family: "Fredoka One";
|
||||
src: url("./assets/fonts/Fredoka One.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Sans";
|
||||
src: url("./assets/fonts/Fira Sans.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("./assets/fonts/Inter.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #27282c;
|
||||
}
|
||||
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
20
src/main.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import "./index.scss";
|
||||
import { IndexedDBProvider } from './contexts/indexedDBContext.tsx';
|
||||
|
||||
interface CustomWindow extends Window {
|
||||
_qdnBase: string;
|
||||
}
|
||||
|
||||
const customWindow = window as unknown as CustomWindow;
|
||||
|
||||
const baseUrl = customWindow?._qdnBase || "";
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter basename={baseUrl}>
|
||||
<IndexedDBProvider>
|
||||
<App />
|
||||
</IndexedDBProvider>
|
||||
</BrowserRouter>,
|
||||
)
|
122
src/pages/Home/Home-Styles.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
export const BubbleBoard = styled(Box)({
|
||||
position: "relative",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(9, 1fr)",
|
||||
gridTemplateRows: "repeat(4, 1fr)",
|
||||
gap: "15px",
|
||||
width: "815px",
|
||||
height: "353px",
|
||||
});
|
||||
|
||||
export const BubbleCard = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "#ffffff05",
|
||||
borderRadius: "50%",
|
||||
fontFamily: "Fredoka One, sans-serif",
|
||||
fontWeight: 500,
|
||||
fontSize: "40px",
|
||||
lineHeight: "48.4px",
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
|
||||
export const BubbleCardColored1 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(124.49deg, #70BAFF 7.03%, #F29999 94.22%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const BubbleCardColored2 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(36.5deg, #70BAFF 19.69%, #F29999 90.73%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const BubbleCardColored3 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(180deg, #70BAFF -24.68%, #ACABD0 25.49%, #F29999 74.03%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const BubbleCardColored4 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(275.71deg, #70BAFF 35.99%, #F29999 95.61%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const MainCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "10px",
|
||||
});
|
||||
|
||||
export const MainRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: "25px",
|
||||
});
|
||||
|
||||
export const PreventPlayingText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fira Sans",
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
fontSize: "18px",
|
||||
lineHeight: "17px",
|
||||
textAlign: "center",
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
// OAuth Button
|
||||
|
||||
export const OAuthButtonRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const OAuthButton = styled(Button)(({ theme }) => ({
|
||||
background: theme.palette.primary.main,
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: "Fira Sans",
|
||||
fontWeight: 600,
|
||||
fontSize: "22px",
|
||||
lineHeight: "17px",
|
||||
padding: "25px 20px",
|
||||
borderRadius: "5px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
background: theme.palette.primary.dark,
|
||||
},
|
||||
}));
|
||||
|
||||
export const HomeWrapper = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "100px",
|
||||
height: "90vh",
|
||||
width: "100%",
|
||||
});
|
106
src/pages/Home/Home.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { FC, useContext, useEffect, useState } from "react";
|
||||
import { AppContainer } from "../../App-styles";
|
||||
|
||||
import axios from "axios";
|
||||
import { Header } from "../../components/header/Header";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import { NotificationContext } from "../../contexts/notificationContext";
|
||||
|
||||
import { TradeOffers } from "../../components/Grids/TradeOffers";
|
||||
import { OngoingTrades } from "../../components/Grids/OngoingTrades";
|
||||
import { Box, Button, CircularProgress } from "@mui/material";
|
||||
import { TextTableTitle } from "../../components/Grids/Table-styles";
|
||||
import { Spacer } from "../../components/common/Spacer";
|
||||
import { ReusableModal } from "../../components/common/reusable-modal/ReusableModal";
|
||||
import { OAuthButton, OAuthButtonRow } from "./Home-Styles";
|
||||
import { CreateSell } from "../../components/sell/CreateSell";
|
||||
|
||||
interface IsInstalledProps {}
|
||||
|
||||
export const HomePage: FC<IsInstalledProps> = ({}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
qortBalance,
|
||||
ltcBalance,
|
||||
userInfo,
|
||||
isAuthenticated,
|
||||
setIsAuthenticated,
|
||||
OAuthLoading,
|
||||
setOAuthLoading,
|
||||
} = useContext(gameContext);
|
||||
const { setNotification } = useContext(NotificationContext);
|
||||
const [mode, setMode] = useState("buy");
|
||||
|
||||
const checkIfAuthenticated = async () => {
|
||||
try {
|
||||
setOAuthLoading(true);
|
||||
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setOAuthLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!userInfo?.address) return;
|
||||
checkIfAuthenticated();
|
||||
}, [userInfo?.address]);
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<Header
|
||||
qortBalance={qortBalance}
|
||||
ltcBalance={ltcBalance}
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
display: mode === "buy" ? "block" : "none",
|
||||
}}
|
||||
>
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
padding: "0 10px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<TextTableTitle
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
My Pending Orders
|
||||
</TextTableTitle>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
<OngoingTrades />
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
padding: "0 10px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<TextTableTitle
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
Open Market Sell Orders
|
||||
</TextTableTitle>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
<TradeOffers ltcBalance={ltcBalance} />
|
||||
</div>
|
||||
|
||||
<CreateSell show={mode === "sell"} qortAddress={userInfo?.address} />
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
42
src/services/socketService/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { DefaultEventsMap } from '@socket.io/component-emitter';
|
||||
|
||||
class SocketService {
|
||||
public socket: Socket | null = null;
|
||||
|
||||
public connect(
|
||||
url: string,
|
||||
token: string
|
||||
): Promise<Socket<DefaultEventsMap, DefaultEventsMap>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = io(url, {
|
||||
auth: {
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.socket) return reject("Socket initialization failed");
|
||||
|
||||
this.socket.on("connect", () => {
|
||||
console.log('connected');
|
||||
resolve(this.socket as Socket);
|
||||
});
|
||||
|
||||
this.socket.on("connect_error", (err) => {
|
||||
console.log("Connection error: ", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null; // Optionally reset the socket to null after disconnecting
|
||||
console.log('Disconnected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new SocketService();
|
||||
|
160
src/styles/theme.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
const commonThemeOptions = createTheme({
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
":root": {
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
html: {
|
||||
scrollBehavior: "smooth",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "inherit",
|
||||
transition: "filter 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
filter: "brightness(1.1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "'Fira Sans', 'Fredoka One', 'Inter'",
|
||||
button: {
|
||||
textTransform: "none",
|
||||
},
|
||||
h1: {
|
||||
fontSize: "42px",
|
||||
},
|
||||
h2: {
|
||||
fontSize: "32px",
|
||||
},
|
||||
h3: {
|
||||
fontSize: "21px",
|
||||
},
|
||||
h4: {
|
||||
fontSize: "18px",
|
||||
},
|
||||
h5: {
|
||||
fontSize: "16px",
|
||||
},
|
||||
h6: {
|
||||
fontSize: "14px",
|
||||
},
|
||||
body1: {
|
||||
fontSize: "1rem",
|
||||
},
|
||||
body2: {
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
},
|
||||
spacing: 8, // Customize the base spacing unit (default is 8)
|
||||
shape: {
|
||||
borderRadius: 4, // Customize the border radius of components
|
||||
},
|
||||
breakpoints: {
|
||||
values: {
|
||||
xs: 0,
|
||||
sm: 600,
|
||||
md: 1160,
|
||||
lg: 1280,
|
||||
xl: 1920,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const darkTheme = createTheme({
|
||||
...commonThemeOptions,
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: "#0085ff",
|
||||
// dark: "#043596",
|
||||
light: "#70BAFF",
|
||||
},
|
||||
secondary: {
|
||||
main: "#F29999",
|
||||
// light: "#5657b1",
|
||||
// dark: "#302F40"
|
||||
},
|
||||
background: {
|
||||
default: "#27282c",
|
||||
},
|
||||
text: {
|
||||
primary: "#ffffff",
|
||||
secondary: "#464646",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
"body::-webkit-scrollbar-track": {
|
||||
backgroundColor: "#27282c",
|
||||
},
|
||||
"body::-webkit-scrollbar-track:hover": {
|
||||
backgroundColor: "#27282c",
|
||||
},
|
||||
"body::-webkit-scrollbar": {
|
||||
width: "16px",
|
||||
height: "10px",
|
||||
backgroundColor: "#27282c",
|
||||
},
|
||||
"body::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#171a27",
|
||||
borderRadius: "8px",
|
||||
backgroundClip: "content-box",
|
||||
border: "4px solid transparent",
|
||||
},
|
||||
"body::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: "#0e1018",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
boxShadow: "none",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
"0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
boxShadow: "none",
|
||||
transition: "filter 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
backgroundColor: "inherit",
|
||||
boxShadow:
|
||||
"0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { darkTheme };
|
8
src/utils/cropAddress.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const cropAddress = (string: string = "", range: number = 5) => {
|
||||
const [start, end] = [
|
||||
string?.substring(0, range),
|
||||
string?.substring(string?.length - range, string?.length),
|
||||
//
|
||||
];
|
||||
return start + "..." + end;
|
||||
};
|
11
src/utils/events.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const executeEvent = (eventName: string, data: any)=> {
|
||||
const event = new CustomEvent(eventName, {detail: data})
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
export const subscribeToEvent = (eventName: string, listener: any)=> {
|
||||
document.addEventListener(eventName, listener)
|
||||
}
|
||||
|
||||
export const unsubscribeFromEvent = (eventName: string, listener: any)=> {
|
||||
document.removeEventListener(eventName, listener)
|
||||
}
|
6
src/utils/formatTime.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function formatTime(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
// Pad the seconds with a leading zero if less than 10
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
26
src/utils/indexedDB.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export const openDatabase = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("tradeDB", 3); // Increment version to trigger upgrade
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as any).result;
|
||||
|
||||
// Create the 'transactions' object store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains("transactions")) {
|
||||
const transactionStore = db.createObjectStore("transactions", { keyPath: "qortalAtAddress" });
|
||||
transactionStore.createIndex("updatedAt", "updatedAt", { unique: false });
|
||||
transactionStore.createIndex("createdAt", "createdAt", { unique: false });
|
||||
}
|
||||
|
||||
// Create a new 'temporarySellOrders' object store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains("temporarySellOrders")) {
|
||||
const temporarySellOrderStore = db.createObjectStore("temporarySellOrders", { keyPath: "atAddress" });
|
||||
temporarySellOrderStore.createIndex("createdAt", "createdAt", { unique: false });
|
||||
temporarySellOrderStore.createIndex("qortAddress", "qortAddress", { unique: false }); // New index for qortAddress
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
};
|
43
src/utils/verifyBalance.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { NotificationProps } from "../contexts/notificationContext";
|
||||
|
||||
export const verifyBalance = async (
|
||||
userInfo: any,
|
||||
setNotification: (notification: NotificationProps) => void,
|
||||
setLoading: (loading: boolean) => void
|
||||
): Promise<boolean> => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!userInfo?.address) {
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "Please connect your wallet",
|
||||
});
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const balanceUrl: string = `/addresses/balance/${userInfo?.address}`;
|
||||
const balanceResponse = await fetch(balanceUrl);
|
||||
const balanceData = await balanceResponse.text();
|
||||
const balanceNumber = Number(balanceData);
|
||||
|
||||
if (isNaN(balanceNumber) || balanceNumber < 1) {
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "You need at least 1 QORT to play",
|
||||
});
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
return true;
|
||||
} catch (error) {
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "An error occurred while checking the balance",
|
||||
});
|
||||
setLoading(false);
|
||||
return false;
|
||||
}
|
||||
};
|
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
11
vite.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react()
|
||||
],
|
||||
|
||||
base: ""
|
||||
})
|