This commit is contained in:
PhilReact 2024-11-10 07:41:27 +02:00
commit aa8f7ffa75
81 changed files with 12142 additions and 0 deletions

18
.eslintrc.cjs Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

26
public/manifest.json Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View 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

View 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

View 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
View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 593 KiB

13
src/assets/SVG/qort.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 562 KiB

3
src/assets/SVG/star.svg Normal file
View 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

View 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

View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/assets/fonts/Inter.ttf Normal file

Binary file not shown.

View 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 };
};

View 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>
);
}

View 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",
}));

View 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
View 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 usethere 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>
);
}

View File

@ -0,0 +1,13 @@
import { Box } from "@mui/material";
export const Spacer = ({ height }: any) => {
return (
<Box
sx={{
height: height,
display: 'flex',
flexShrink: 0
}}
/>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@ -0,0 +1,7 @@
export interface IconTypes {
color: string;
height: string;
width: string;
className?: string;
onClickFunc?: () => void;
}

View 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>
);
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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
/>
);
};

View File

@ -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"
}));

View 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 />}
</>
);
};

View 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
};
};

View 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%",
});

View 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",
}));

View 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>
);
};

View 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>
)
}

View 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
View File

@ -0,0 +1,2 @@
export const playingAmount = 0.25;
export const homeAddress = 'Qa4cxK73TXWgA8fXs7sK1fp96syCQTSTmQ'

View 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);

View 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);
};

View 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: () => {},
});

View 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: () => {},
});

View 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
View 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>;
}
}

View 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
View 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
View 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>,
)

View 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
View 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>
);
};

View 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
View 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
View 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
View 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
View 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
View 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);
});
};

View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

25
tsconfig.json Normal file
View 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
View 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
View 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: ""
})