first commit
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
18
client/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
30
client/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
16
client/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<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 Games</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4348
client/package-lock.json
generated
Normal file
44
client/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
BIN
client/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
client/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 834 B |
BIN
client/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
client/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
client/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
client/src/App-styles.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
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: "center",
|
||||
padding: "1em 0",
|
||||
}));
|
||||
|
||||
export const MainContainer = styled(Box)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
38
client/src/App.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
312
client/src/App.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import "./App.css";
|
||||
import socketService from "./services/socketService";
|
||||
import GameContext, {
|
||||
IGameContextProps,
|
||||
Player,
|
||||
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";
|
||||
|
||||
// Initialize Google Analytics
|
||||
ReactGA.initialize("G-J3QYNDDK5N");
|
||||
|
||||
export const isExtensionInstalledFunc = async () => {
|
||||
try {
|
||||
const response = await sendRequestToExtension(
|
||||
"REQUEST_IS_INSTALLED",
|
||||
{},
|
||||
750
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
}
|
||||
};
|
||||
|
||||
export async function requestConnection() {
|
||||
try {
|
||||
const response = await sendRequestToExtension("REQUEST_CONNECTION");
|
||||
console.log("User info response:", response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error requesting user info:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestAuthentication() {
|
||||
try {
|
||||
const response = await sendRequestToExtension(
|
||||
"REQUEST_AUTHENTICATION",
|
||||
{},
|
||||
90000
|
||||
);
|
||||
console.log("AUTH info response:", response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error requesting user info:", error);
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export let serverUrl: string;
|
||||
if (import.meta.env.MODE === "production") {
|
||||
serverUrl = "https://www.qort.games";
|
||||
} else {
|
||||
serverUrl = "http://localhost:3001";
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [gameWinner, setGameWinner] = useState<"R" | "B" | "TIE" | null>(null);
|
||||
const [isInRoom, setInRoom] = useState(false);
|
||||
const [playerSymbol, setPlayerSymbol] = useState<"R" | "B">("R");
|
||||
const [isPlayerTurn, setPlayerTurn] = useState(false);
|
||||
const [isGameStarted, setGameStarted] = useState(false);
|
||||
const [players, setPlayers] = useState<Record<string, Player>>({});
|
||||
const [game, setGame] = useState<any>(null);
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [isSocketUp, setIsSocketUp] = useState<boolean>(false);
|
||||
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 [loadingGame, setLoadingGame] = useState<boolean>(false);
|
||||
|
||||
const requestUserInfo = async () => {
|
||||
setLoadingSlider(true);
|
||||
try {
|
||||
const response = await sendRequestToExtension("REQUEST_USER_INFO");
|
||||
console.log("User info response:", response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error requesting user info:", error);
|
||||
} finally {
|
||||
setLoadingSlider(false);
|
||||
}
|
||||
};
|
||||
|
||||
const connectSocket = async () => {
|
||||
// If there's no token in local storage, do not connect the socket and hence show oauth button
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
const socket = await socketService
|
||||
.connect(serverUrl, token)
|
||||
.then(() => {
|
||||
setIsSocketUp(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Error: ", err);
|
||||
})
|
||||
};
|
||||
|
||||
const resetNotification = () => {
|
||||
setNotification({ alertType: "", msg: "" });
|
||||
};
|
||||
|
||||
const gameContextValue: IGameContextProps = {
|
||||
gameWinner,
|
||||
setGameWinner,
|
||||
isInRoom,
|
||||
setInRoom,
|
||||
playerSymbol,
|
||||
setPlayerSymbol,
|
||||
isPlayerTurn,
|
||||
setPlayerTurn,
|
||||
isGameStarted,
|
||||
setGameStarted,
|
||||
setPlayers,
|
||||
players,
|
||||
game,
|
||||
setGame,
|
||||
userInfo,
|
||||
setUserInfo,
|
||||
userNameAvatar,
|
||||
setUserNameAvatar,
|
||||
};
|
||||
|
||||
const userContextValue: UserContextProps = {
|
||||
avatar,
|
||||
setAvatar,
|
||||
};
|
||||
|
||||
const notificationContextValue = {
|
||||
notification,
|
||||
setNotification,
|
||||
resetNotification,
|
||||
};
|
||||
|
||||
const loadingContextValue = {
|
||||
loadingSlider,
|
||||
setLoadingSlider,
|
||||
loadingGame,
|
||||
setLoadingGame,
|
||||
};
|
||||
|
||||
const isInstalledFunc = async () => {
|
||||
try {
|
||||
const res = await isExtensionInstalledFunc();
|
||||
if (!res?.version) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Extension not installed",
|
||||
userInfo: null,
|
||||
};
|
||||
}
|
||||
const res2 = await requestConnection();
|
||||
if (res2 === true) {
|
||||
const res3 = await requestAuthentication();
|
||||
if (res3 === true) {
|
||||
const res4 = await requestUserInfo();
|
||||
if (res4?.address) {
|
||||
setUserInfo(res4);
|
||||
return {
|
||||
success: true,
|
||||
message: "Logged in successfully!",
|
||||
userInfo: res4,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: "Error requesting user info",
|
||||
userInfo: null,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: "Error authenticating user",
|
||||
userInfo: null,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: "Error requesting user info",
|
||||
userInfo: null,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Error requesting user info",
|
||||
userInfo: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// connectSocket();
|
||||
// return () => {
|
||||
// socketService.disconnect(); // You would need to implement this method
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
isInstalledFunc();
|
||||
}, 750);
|
||||
}, []);
|
||||
|
||||
const handleMessage = (event: any) => {
|
||||
if (event.data.type === "LOGOUT") {
|
||||
console.log("Logged out from extension");
|
||||
setUserInfo(null);
|
||||
setAvatar("");
|
||||
setIsSocketUp(false);
|
||||
localStorage.setItem("token", "");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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
|
||||
connectSocket={() => connectSocket()}
|
||||
isSocketUp={isSocketUp}
|
||||
isInstalledFunc={isInstalledFunc}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
</GameContext.Provider>
|
||||
</UserContext.Provider>
|
||||
</LoadingContext.Provider>
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
3
client/src/assets/SVG/caretDown.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="8" viewBox="0 0 15 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1L7.5 7L14 1" stroke="#464646" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 169 B |
1
client/src/assets/SVG/closeIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z"/></svg>
|
||||
|
After Width: | Height: | Size: 201 B |
1
client/src/assets/SVG/doubleCaretRight.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M383-480 200-664l56-56 240 240-240 240-56-56 183-184Zm264 0L464-664l56-56 240 240-240 240-56-56 183-184Z"/></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
3
client/src/assets/SVG/home.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="19" viewBox="0 0 21 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.64729 18.9972C3.27267 18.9972 2.96761 18.685 2.96761 18.2955V10.7916H1.18276H1.18009C0.527159 10.7916 0 10.2446 0 9.57322C0 9.18918 0.173937 8.84659 0.44153 8.6228L2.75086 6.5562V3.11371C2.75086 2.71034 3.06662 2.38432 3.45463 2.38432H4.91569C5.30637 2.38432 5.62213 2.71034 5.62213 3.11371V3.98124L9.72701 0.298386C10.1712 -0.099462 10.8295 -0.099462 11.2737 0.298386L20.594 8.65319C20.9659 8.98749 21.0971 9.52348 20.9258 10.0015C20.7519 10.4767 20.313 10.7944 19.8206 10.7944H18.0331V18.2982C18.0331 18.6878 17.7281 19 17.3534 19H13.3368V13.5406C13.3368 12.7698 12.7321 12.1454 11.9855 12.1454H9.01254C8.26595 12.1454 7.66119 12.7698 7.66119 13.5406V18.9945H3.64729V18.9972Z" fill="#464646"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 811 B |
3
client/src/assets/SVG/info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 0C5.61553 0 4.26215 0.410543 3.11101 1.17971C1.95987 1.94888 1.06266 3.04213 0.532846 4.32121C0.00303299 5.6003 -0.13559 7.00776 0.134506 8.36563C0.404603 9.7235 1.07129 10.9708 2.05026 11.9497C3.02922 12.9287 4.2765 13.5954 5.63437 13.8655C6.99224 14.1356 8.3997 13.997 9.67879 13.4672C10.9579 12.9373 12.0511 12.0401 12.8203 10.889C13.5895 9.73784 14 8.38447 14 7C13.9979 5.14412 13.2598 3.36484 11.9475 2.05253C10.6352 0.740225 8.85588 0.002064 7 0ZM7 12.6C5.89243 12.6 4.80972 12.2716 3.88881 11.6562C2.96789 11.0409 2.25013 10.1663 1.82628 9.14302C1.40243 8.11976 1.29153 6.99379 1.50761 5.90749C1.72368 4.8212 2.25703 3.82337 3.0402 3.0402C3.82338 2.25703 4.8212 1.72368 5.9075 1.5076C6.99379 1.29153 8.11976 1.40242 9.14303 1.82627C10.1663 2.25012 11.0409 2.96789 11.6562 3.88881C12.2716 4.80972 12.6 5.89242 12.6 7C12.5983 8.48469 12.0078 9.90808 10.9579 10.9579C9.90809 12.0078 8.48469 12.5983 7 12.6ZM7.7 6.3V9.8C7.7 9.98565 7.62625 10.1637 7.49498 10.295C7.3637 10.4262 7.18565 10.5 7 10.5C6.81435 10.5 6.6363 10.4262 6.50503 10.295C6.37375 10.1637 6.3 9.98565 6.3 9.8V7C6.11435 7 5.9363 6.92625 5.80503 6.79497C5.67375 6.6637 5.6 6.48565 5.6 6.3C5.6 6.11435 5.67375 5.9363 5.80503 5.80502C5.9363 5.67375 6.11435 5.6 6.3 5.6H7C7.18565 5.6 7.3637 5.67375 7.49498 5.80502C7.62625 5.9363 7.7 6.11435 7.7 6.3ZM7.7 4.2C7.7 4.33845 7.65895 4.47378 7.58203 4.5889C7.50511 4.70401 7.39579 4.79373 7.26788 4.84671C7.13997 4.8997 6.99923 4.91356 6.86344 4.88655C6.72765 4.85954 6.60292 4.79287 6.50503 4.69497C6.40713 4.59708 6.34046 4.47235 6.31345 4.33656C6.28644 4.20078 6.3003 4.06003 6.35329 3.93212C6.40627 3.80421 6.49599 3.69489 6.6111 3.61797C6.72622 3.54105 6.86156 3.5 7 3.5C7.18565 3.5 7.3637 3.57375 7.49498 3.70502C7.62625 3.8363 7.7 4.01435 7.7 4.2Z" fill="#464646"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
13
client/src/assets/SVG/qort-white.svg
Normal file
|
After Width: | Height: | Size: 593 KiB |
13
client/src/assets/SVG/qort.svg
Normal file
|
After Width: | Height: | Size: 562 KiB |
3
client/src/assets/SVG/star.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="34" viewBox="0 0 36 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.7818 21.7029C28.6179 21.8647 28.542 22.094 28.5799 22.3203L29.8898 31.6695C29.993 32.4158 29.6727 33.1591 29.055 33.6026C28.4388 34.0462 27.6252 34.1211 26.9362 33.7945L18.3259 29.6705C18.1195 29.5716 17.8797 29.5716 17.6732 29.6705L9.06295 33.7945C8.37389 34.1241 7.55731 34.0537 6.93652 33.6086C6.31725 33.1636 5.99701 32.4188 6.10176 31.6695L7.41161 22.3203C7.44955 22.094 7.37366 21.8647 7.20974 21.7029L0.592327 14.9012C0.0520072 14.3573 -0.136207 13.5616 0.100576 12.8378C0.337351 12.1125 0.962693 11.5775 1.72309 11.4486L11.1334 9.79128C11.3626 9.75381 11.5584 9.61145 11.6631 9.40765L16.1711 1.08343H16.1696C16.5308 0.416576 17.2335 0 18 0C18.7665 0 19.4692 0.416576 19.8304 1.08343L24.3384 9.40765H24.3369C24.4416 9.61145 24.6374 9.75381 24.8666 9.79128L34.2769 11.4486C35.0373 11.5775 35.6626 12.1125 35.8994 12.8378C36.1362 13.5616 35.948 14.3573 35.4077 14.9012L28.7818 21.7029Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
client/src/assets/SVG/volumeOff.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Zm-80 238v-94l-72-72H200v80h114l86 86Zm-36-130Z"/></svg>
|
||||
|
After Width: | Height: | Size: 495 B |
1
client/src/assets/SVG/volumeOn.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 378 B |
BIN
client/src/assets/fonts/FiraSans-Medium.ttf
Normal file
BIN
client/src/assets/fonts/FiraSans-Regular.ttf
Normal file
BIN
client/src/assets/fonts/Fredoka One.ttf
Normal file
BIN
client/src/assets/fonts/Inter.ttf
Normal file
264
client/src/components/Grids/TradeOffers.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { ColDef } 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 { Button } from '@mui/material';
|
||||
|
||||
interface RowData {
|
||||
amountQORT: number;
|
||||
priceUSD: number;
|
||||
totalUSD: number;
|
||||
seller: string;
|
||||
}
|
||||
|
||||
export const TradeOffers: React.FC = () => {
|
||||
const [offers, setOffers] = useState<any[]>([])
|
||||
const [selectedOffer, setSelectedOffer] = useState(null)
|
||||
const tradePresenceTxns = useRef(null)
|
||||
const offeringTrades = useRef<any[]>([])
|
||||
const blockedTradesList = useRef([])
|
||||
const columnDefs: ColDef[] = [
|
||||
{ headerName: "Amount (QORT)", field: "qortAmount" },
|
||||
{ headerName: "Price (LTC)", valueGetter: (params) => +params.data.foreignAmount / +params.data.qortAmount, sortable: true, sort: 'asc' },
|
||||
{ headerName: "Total (LTC)", field: "foreignAmount", },
|
||||
{ headerName: "Seller", field: "qortalCreator" }
|
||||
];
|
||||
|
||||
const rowData: RowData[] = [
|
||||
{ amountQORT: 100, priceUSD: 2, totalUSD: 200, seller: "Seller1" },
|
||||
{ amountQORT: 50, priceUSD: 2.5, totalUSD: 125, seller: "Seller2" },
|
||||
// Add more rows as needed
|
||||
];
|
||||
|
||||
const onRowClicked = (event: any) => {
|
||||
setSelectedOffer(event.data)
|
||||
console.log("Row clicked: ", event.data);
|
||||
// Handle the row click callback with event.data
|
||||
};
|
||||
|
||||
const restartTradePresenceWebSocket = () => {
|
||||
setTimeout(() => initTradePresenceWebSocket(true), 50)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getNewBlockedTrades = async () => {
|
||||
const unconfirmedTransactionsList = async () => {
|
||||
|
||||
const unconfirmedTransactionslUrl = `https://api.qortal.org/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
|
||||
}, [])
|
||||
localStorage.setItem("failedTrades", JSON.stringify(cleanBlockedTrades))
|
||||
blockedTradesList.current = JSON.parse(localStorage.getItem("failedTrades") || "[]")
|
||||
}
|
||||
|
||||
await filterUnconfirmedTransactionsList()
|
||||
}
|
||||
|
||||
const processOffersWithPresence = () => {
|
||||
if (offeringTrades.current === null) return
|
||||
console.log('offeringTrades.current', offeringTrades.current)
|
||||
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 () => {
|
||||
console.log('tradePresenceTxns.current', tradePresenceTxns.current)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('second', offeringTrades.current)
|
||||
let filteredOffers = offeringTrades.current.filter((offeringTrade) => filterOffersUsingTradePresence(offeringTrade))
|
||||
console.log({filteredOffers})
|
||||
let tradesPresenceCleaned: any[] = filteredOffers
|
||||
|
||||
|
||||
|
||||
blockedTradesList.current.forEach((item: any) => {
|
||||
const toDelete = item.recipient
|
||||
tradesPresenceCleaned = tradesPresenceCleaned.filter(el => {
|
||||
return el.qortalCreatorTradeAddress !== toDelete
|
||||
})
|
||||
})
|
||||
|
||||
console.log({tradesPresenceCleaned})
|
||||
if(tradesPresenceCleaned){
|
||||
setOffers(tradesPresenceCleaned)
|
||||
}
|
||||
// self.postMessage({ type: 'PRESENCE', data: { offers: offeringTrades.current, filteredOffers: filteredOffers, relatedCoin: _relatedCoin } })
|
||||
}
|
||||
|
||||
startOfferPresenceMapping()
|
||||
}
|
||||
|
||||
const restartTradeOffersWebSocket = () => {
|
||||
setTimeout(() => initTradeOffersWebSocket(true), 50)
|
||||
}
|
||||
|
||||
const initTradePresenceWebSocket = (restarted = false) => {
|
||||
let socketTimeout: any
|
||||
let socketLink = `ws://host4data.qortal.org:12391/websockets/crosschain/tradepresence`
|
||||
const socket = new WebSocket(socketLink)
|
||||
socket.onopen = () => {
|
||||
setTimeout(pingSocket, 50)
|
||||
}
|
||||
socket.onmessage = (e) => {
|
||||
console.log('data', JSON.parse(e.data))
|
||||
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 = `ws://host4data.qortal.org:12391/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)]
|
||||
console.log('offers', 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)
|
||||
}
|
||||
}
|
||||
|
||||
// const fetchTradeOffers = async () => {
|
||||
// try {
|
||||
// const response = await axios.get('https://api.qortal.org/crosschain/tradeoffers?foreignBlockchain=LITECOIN&reverse=true&limit=100');
|
||||
// setOffers(response.data);
|
||||
// } catch (error) {
|
||||
// console.error('Error fetching trade offers:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// // Fetch trade offers immediately
|
||||
// fetchTradeOffers();
|
||||
|
||||
// // Set up interval to fetch trade offers every 30 seconds
|
||||
// const interval = setInterval(() => {
|
||||
// fetchTradeOffers();
|
||||
// }, 30000); // 30000 milliseconds = 30 seconds
|
||||
|
||||
// // Clean up the interval on component unmount
|
||||
// return () => clearInterval(interval);
|
||||
// }, []);
|
||||
|
||||
useEffect(()=> {
|
||||
blockedTradesList.current = JSON.parse(localStorage.getItem('failedTrades') || '[]')
|
||||
initTradePresenceWebSocket()
|
||||
initTradeOffersWebSocket()
|
||||
getNewBlockedTrades()
|
||||
const intervalBlockTrades = setInterval(() => {
|
||||
getNewBlockedTrades()
|
||||
}, 150000)
|
||||
|
||||
return ()=> {
|
||||
clearInterval(intervalBlockTrades)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const buyOrder = async ()=> {
|
||||
try {
|
||||
if(!selectedOffer) return
|
||||
console.log({selectedOffer})
|
||||
const response = await sendRequestToExtension(
|
||||
"REQUEST_BUY_ORDER",
|
||||
{
|
||||
qortalAtAddress: selectedOffer?.qortalAtAddress
|
||||
},
|
||||
60000
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
console.log({offers})
|
||||
return (
|
||||
<div className="ag-theme-alpine" style={{ height: 400, width: '100%' }}>
|
||||
<AgGridReact
|
||||
columnDefs={columnDefs}
|
||||
rowData={offers}
|
||||
onRowClicked={onRowClicked}
|
||||
rowSelection="single"
|
||||
/>
|
||||
{selectedOffer && (
|
||||
<Button onClick={buyOrder}>Buy</Button>
|
||||
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
111
client/src/components/JoinRoom/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Box,
|
||||
TextField,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
import gameService from "../../services/gameService";
|
||||
import socketService from "../../services/socketService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface IJoinRoomProps {
|
||||
userAddress: "";
|
||||
}
|
||||
|
||||
const JoinRoomContainer = styled("div")`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 2em;
|
||||
`;
|
||||
|
||||
const RoomIdInput = styled("input")`
|
||||
height: 30px;
|
||||
width: 20em;
|
||||
font-size: 17px;
|
||||
outline: none;
|
||||
border: 1px solid #8e44ad;
|
||||
border-radius: 3px;
|
||||
padding: 0 10px;
|
||||
`;
|
||||
|
||||
const JoinButton = styled("button")`
|
||||
outline: none;
|
||||
background-color: #8e44ad;
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 5px;
|
||||
padding: 4px 18px;
|
||||
transition: all 230ms ease-in-out;
|
||||
margin-top: 1em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
border: 2px solid #8e44ad;
|
||||
color: #8e44ad;
|
||||
}
|
||||
`;
|
||||
|
||||
export function JoinRoom(props: IJoinRoomProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [roomName, setRoomName] = useState("");
|
||||
const [isJoining, setJoining] = useState(false);
|
||||
|
||||
const { setInRoom, isInRoom, setGame } = useContext(gameContext);
|
||||
|
||||
const handleRoomNameChange = (e: React.ChangeEvent<any>) => {
|
||||
const value = e.target.value;
|
||||
setRoomName(value);
|
||||
};
|
||||
|
||||
const joinRoom = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!props.userAddress) return;
|
||||
const socket = socketService.socket;
|
||||
if (!roomName || roomName.trim() === "" || !socket) return;
|
||||
setJoining(true);
|
||||
|
||||
const joined = await gameService
|
||||
.joinGameRoom(socket, roomName, props.userAddress)
|
||||
.catch((err) => {
|
||||
alert(err);
|
||||
});
|
||||
console.log(joined);
|
||||
if (joined) {
|
||||
setInRoom(true);
|
||||
if (joined?.game) {
|
||||
setGame(joined.game);
|
||||
}
|
||||
navigate("/game");
|
||||
}
|
||||
setJoining(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={joinRoom}>
|
||||
<JoinRoomContainer>
|
||||
<h4>Enter Room ID to Join the Game</h4>
|
||||
<RoomIdInput
|
||||
placeholder="Room ID"
|
||||
value={roomName}
|
||||
onChange={handleRoomNameChange}
|
||||
/>
|
||||
<JoinButton type="submit" disabled={isJoining}>
|
||||
{isJoining ? "Joining..." : "Join"}
|
||||
</JoinButton>
|
||||
</JoinRoomContainer>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
15
client/src/components/common/icons/CaretDownSVG.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CaretDownSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 15 8"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 1L7.5 7L14 1" stroke="#464646" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
23
client/src/components/common/icons/CloseSVG.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CloseSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
onClickFunc,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<div className={className} onClick={onClickFunc}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
21
client/src/components/common/icons/DoubleCaretRightSVG.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const DoubleCaretRightSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M383-480 200-664l56-56 240 240-240 240-56-56 183-184Zm264 0L464-664l56-56 240 240-240 240-56-56 183-184Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
24
client/src/components/common/icons/HomeSVG.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const HomeSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 21 19"
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.64729 18.9972C3.27267 18.9972 2.96761 18.685 2.96761 18.2955V10.7916H1.18276H1.18009C0.527159 10.7916 0 10.2446 0 9.57322C0 9.18918 0.173937 8.84659 0.44153 8.6228L2.75086 6.5562V3.11371C2.75086 2.71034 3.06662 2.38432 3.45463 2.38432H4.91569C5.30637 2.38432 5.62213 2.71034 5.62213 3.11371V3.98124L9.72701 0.298386C10.1712 -0.099462 10.8295 -0.099462 11.2737 0.298386L20.594 8.65319C20.9659 8.98749 21.0971 9.52348 20.9258 10.0015C20.7519 10.4767 20.313 10.7944 19.8206 10.7944H18.0331V18.2982C18.0331 18.6878 17.7281 19 17.3534 19H13.3368V13.5406C13.3368 12.7698 12.7321 12.1454 11.9855 12.1454H9.01254C8.26595 12.1454 7.66119 12.7698 7.66119 13.5406V18.9945H3.64729V18.9972Z"
|
||||
fill="#464646"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
7
client/src/components/common/icons/IconTypes.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface IconTypes {
|
||||
color: string;
|
||||
height: string;
|
||||
width: string;
|
||||
className?: string;
|
||||
onClickFunc?: () => void;
|
||||
}
|
||||
16
client/src/components/common/icons/InfoSVG.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const InfoSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
<svg onClick={onClickFunc} className={className} width={width} height={height} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 0C5.61553 0 4.26215 0.410543 3.11101 1.17971C1.95987 1.94888 1.06266 3.04213 0.532846 4.32121C0.00303299 5.6003 -0.13559 7.00776 0.134506 8.36563C0.404603 9.7235 1.07129 10.9708 2.05026 11.9497C3.02922 12.9287 4.2765 13.5954 5.63437 13.8655C6.99224 14.1356 8.3997 13.997 9.67879 13.4672C10.9579 12.9373 12.0511 12.0401 12.8203 10.889C13.5895 9.73784 14 8.38447 14 7C13.9979 5.14412 13.2598 3.36484 11.9475 2.05253C10.6352 0.740225 8.85588 0.002064 7 0ZM7 12.6C5.89243 12.6 4.80972 12.2716 3.88881 11.6562C2.96789 11.0409 2.25013 10.1663 1.82628 9.14302C1.40243 8.11976 1.29153 6.99379 1.50761 5.90749C1.72368 4.8212 2.25703 3.82337 3.0402 3.0402C3.82338 2.25703 4.8212 1.72368 5.9075 1.5076C6.99379 1.29153 8.11976 1.40242 9.14303 1.82627C10.1663 2.25012 11.0409 2.96789 11.6562 3.88881C12.2716 4.80972 12.6 5.89242 12.6 7C12.5983 8.48469 12.0078 9.90808 10.9579 10.9579C9.90809 12.0078 8.48469 12.5983 7 12.6ZM7.7 6.3V9.8C7.7 9.98565 7.62625 10.1637 7.49498 10.295C7.3637 10.4262 7.18565 10.5 7 10.5C6.81435 10.5 6.6363 10.4262 6.50503 10.295C6.37375 10.1637 6.3 9.98565 6.3 9.8V7C6.11435 7 5.9363 6.92625 5.80503 6.79497C5.67375 6.6637 5.6 6.48565 5.6 6.3C5.6 6.11435 5.67375 5.9363 5.80503 5.80502C5.9363 5.67375 6.11435 5.6 6.3 5.6H7C7.18565 5.6 7.3637 5.67375 7.49498 5.80502C7.62625 5.9363 7.7 6.11435 7.7 6.3ZM7.7 4.2C7.7 4.33845 7.65895 4.47378 7.58203 4.5889C7.50511 4.70401 7.39579 4.79373 7.26788 4.84671C7.13997 4.8997 6.99923 4.91356 6.86344 4.88655C6.72765 4.85954 6.60292 4.79287 6.50503 4.69497C6.40713 4.59708 6.34046 4.47235 6.31345 4.33656C6.28644 4.20078 6.3003 4.06003 6.35329 3.93212C6.40627 3.80421 6.49599 3.69489 6.6111 3.61797C6.72622 3.54105 6.86156 3.5 7 3.5C7.18565 3.5 7.3637 3.57375 7.49498 3.70502C7.62625 3.8363 7.7 4.01435 7.7 4.2Z" fill={color}/>
|
||||
</svg>
|
||||
|
||||
);
|
||||
};
|
||||
68
client/src/components/common/icons/QortalLogoSVG.tsx
Normal file
53
client/src/components/common/icons/QortalLogoWhiteSVG.tsx
Normal file
25
client/src/components/common/icons/StarSVG.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const StarSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 36 34"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M28.7818 21.7029C28.6179 21.8647 28.542 22.094 28.5799 22.3203L29.8898 31.6695C29.993 32.4158 29.6727 33.1591 29.055 33.6026C28.4388 34.0462 27.6252 34.1211 26.9362 33.7945L18.3259 29.6705C18.1195 29.5716 17.8797 29.5716 17.6732 29.6705L9.06295 33.7945C8.37389 34.1241 7.55731 34.0537 6.93652 33.6086C6.31725 33.1636 5.99701 32.4188 6.10176 31.6695L7.41161 22.3203C7.44955 22.094 7.37366 21.8647 7.20974 21.7029L0.592327 14.9012C0.0520072 14.3573 -0.136207 13.5616 0.100576 12.8378C0.337351 12.1125 0.962693 11.5775 1.72309 11.4486L11.1334 9.79128C11.3626 9.75381 11.5584 9.61145 11.6631 9.40765L16.1711 1.08343H16.1696C16.5308 0.416576 17.2335 0 18 0C18.7665 0 19.4692 0.416576 19.8304 1.08343L24.3384 9.40765H24.3369C24.4416 9.61145 24.6374 9.75381 24.8666 9.79128L34.2769 11.4486C35.0373 11.5775 35.6626 12.1125 35.8994 12.8378C36.1362 13.5616 35.948 14.3573 35.4077 14.9012L28.7818 21.7029Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
23
client/src/components/common/icons/VolumeOffSVG.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const VolumeOffSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M792-56 671-177q-25 16-53 27.5T560-131v-82q14-5 27.5-10t25.5-12L480-368v208L280-360H120v-240h128L56-792l56-56 736 736-56 56Zm-8-232-58-58q17-31 25.5-65t8.5-70q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 53-14.5 102T784-288ZM650-422l-90-90v-130q47 22 73.5 66t26.5 96q0 15-2.5 29.5T650-422ZM480-592 376-696l104-104v208Zm-80 238v-94l-72-72H200v80h114l86 86Zm-36-130Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
23
client/src/components/common/icons/VolumeOnSVG.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const VolumeOnSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
53
client/src/components/common/notification/Notification.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useContext, useEffect } from "react";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import { Zoom } from "react-toastify";
|
||||
import { NotificationContext } from "../../../contexts/notificationContext";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
|
||||
export const Notification = () => {
|
||||
|
||||
const { notification } = useContext(NotificationContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (notification?.alertType === "alertError") {
|
||||
toast.error(`❌ ${notification?.msg}`, {
|
||||
position: "bottom-right",
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
toastId: "error",
|
||||
});
|
||||
}
|
||||
if (notification?.alertType === "alertSuccess") {
|
||||
toast.success(`✔️ ${notification?.msg}`, {
|
||||
position: "bottom-right",
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
toastId: "success",
|
||||
});
|
||||
}
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ToastContainer
|
||||
transition={Zoom}
|
||||
position="bottom-right"
|
||||
autoClose={false}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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: "absolute",
|
||||
top: "0",
|
||||
transform: "translateY(40%)",
|
||||
width: "629px",
|
||||
minHeight: "446px",
|
||||
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"
|
||||
}));
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ReusableModalBackdrop,
|
||||
ReusableModalContainer,
|
||||
ReusableModalSubContainer,
|
||||
} from "./ReusableModal-styles";
|
||||
interface ReusableModalProps {
|
||||
backdrop?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ReusableModal: React.FC<ReusableModalProps> = ({ backdrop, children }) => {
|
||||
return (
|
||||
<>
|
||||
<ReusableModalContainer>
|
||||
<ReusableModalSubContainer>{children}</ReusableModalSubContainer>
|
||||
</ReusableModalContainer>
|
||||
{backdrop && <ReusableModalBackdrop />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
64
client/src/components/game/Game-styles.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
interface ICellProps {
|
||||
borderTopStyle?: boolean;
|
||||
borderRightStyle?: boolean;
|
||||
borderLeftStyle?: boolean;
|
||||
borderBottomStyle?: boolean;
|
||||
}
|
||||
|
||||
export const Cell = styled(Box)<ICellProps>(
|
||||
({
|
||||
borderTopStyle,
|
||||
borderLeftStyle,
|
||||
borderBottomStyle,
|
||||
borderRightStyle,
|
||||
}) => ({
|
||||
width: "13em",
|
||||
height: "9em",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "20px",
|
||||
cursor: "pointer",
|
||||
borderTop: borderTopStyle ? "3px solid #8e44ad" : "none",
|
||||
borderLeft: borderLeftStyle ? "3px solid #8e44ad" : "none",
|
||||
borderBottom: borderBottomStyle ? "3px solid #8e44ad" : "none",
|
||||
borderRight: borderRightStyle ? "3px solid #8e44ad" : "none",
|
||||
transition: "all 270ms ease-in-out",
|
||||
"&:hover": {
|
||||
backgroundColor: "#8d44ad28",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const X = styled("span")({
|
||||
fontSize: "100px",
|
||||
color: "#8e44ad",
|
||||
"&::after": {
|
||||
content: "X",
|
||||
},
|
||||
});
|
||||
|
||||
export const O = styled("span")({
|
||||
fontSize: "100px",
|
||||
color: "#8e44ad",
|
||||
"&::after": {
|
||||
content: "O",
|
||||
},
|
||||
});
|
||||
|
||||
export const GameContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "100px 0 50px 0",
|
||||
gap: "15px",
|
||||
fontFamily: "Zen Tokyo Zoo, cursive",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const RowContainer = styled(Box)({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
});
|
||||
134
client/src/components/header/Header-styles.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
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 AvatarCircle = styled("img")({
|
||||
borderRadius: "50%",
|
||||
width: "35px",
|
||||
height: "35px",
|
||||
objectFit: "cover",
|
||||
userSelect: "none",
|
||||
});
|
||||
118
client/src/components/header/Header.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useState, useEffect, useRef, useContext } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import {
|
||||
AvatarCircle,
|
||||
CaretDownIcon,
|
||||
DropdownContainer,
|
||||
GameSelectDropdown,
|
||||
GameSelectDropdownMenu,
|
||||
GameSelectDropdownMenuItem,
|
||||
HeaderNav,
|
||||
HomeIcon,
|
||||
NameRow,
|
||||
QortalLogoIcon,
|
||||
Username,
|
||||
} from "./Header-styles";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
import { findUsableApi } from "../../utils/findUsableApi";
|
||||
import { UserContext } from "../../contexts/userContext";
|
||||
import { cropAddress } from "../../utils/cropAddress";
|
||||
import { BubbleCardColored1 } from "../../pages/Home/Home-Styles";
|
||||
|
||||
|
||||
export const Header = () => {
|
||||
const [openDropdown, setOpenDropdown] = useState<boolean>(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { userInfo } = useContext(gameContext);
|
||||
const { avatar, setAvatar } = useContext(UserContext);
|
||||
|
||||
const getAvatar = async () => {
|
||||
const validApi: string = await findUsableApi();
|
||||
type Base64String = string;
|
||||
try {
|
||||
const avatarUrl: string = `${validApi}/arbitrary/THUMBNAIL/${userInfo?.name}/qortal_avatar?encoding=base64&rebuild=false`;
|
||||
|
||||
const avatarResponse = await fetch(avatarUrl);
|
||||
const avatar: string | Base64String = await avatarResponse.text();
|
||||
|
||||
setAvatar(avatar);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setAvatar("No Avatar");
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<DropdownContainer>
|
||||
<GameSelectDropdown
|
||||
ref={buttonRef}
|
||||
onClick={() => setOpenDropdown(!openDropdown)}
|
||||
>
|
||||
QONNECT4 <CaretDownIcon height="8" width="15" color="none" />
|
||||
</GameSelectDropdown>
|
||||
<HomeIcon height="19" width="21" color="none" />
|
||||
{openDropdown && (
|
||||
<GameSelectDropdownMenu ref={dropdownRef}>
|
||||
<GameSelectDropdownMenuItem>
|
||||
QONNECT FOUR
|
||||
</GameSelectDropdownMenuItem>
|
||||
</GameSelectDropdownMenu>
|
||||
)}
|
||||
</DropdownContainer>
|
||||
<NameRow>
|
||||
{userInfo?.name ? (
|
||||
<Username>{userInfo?.name}</Username>
|
||||
) : userInfo?.address ? (
|
||||
<Username>{cropAddress(userInfo?.address)}</Username>
|
||||
) : null}
|
||||
{avatar ? (
|
||||
<AvatarCircle
|
||||
src={`data:image/jpeg;base64,${avatar}`}
|
||||
alt={`${userInfo?.name}'s Avatar`}
|
||||
/>
|
||||
) : !avatar && userInfo?.address ? (
|
||||
<BubbleCardColored1 style={{ height: "35px", width: "35px" }} />
|
||||
) : (
|
||||
<QortalLogoIcon height="35" width="35" color="none" onClickFunc={() => {
|
||||
window.open("https://www.qortal.dev/extension", "_blank")?.focus();
|
||||
ReactGA.event({
|
||||
category: "Redirect",
|
||||
action: "Clicked the Qortal Logo on the home page to redirect to Qortal.dev/extension",
|
||||
label: "Clicked the Qortal Logo on the home page to redirect to Qortal.dev/extension"
|
||||
});
|
||||
}
|
||||
} />
|
||||
)}
|
||||
</NameRow>
|
||||
</HeaderNav>
|
||||
);
|
||||
};
|
||||
92
client/src/components/leaderboard/Leaderboard-styles.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Box, Button, Typography, keyframes } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { CloseSVG } from "../common/icons/CloseSVG";
|
||||
|
||||
const leaderBoardAnimation = keyframes`
|
||||
0% {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeaderboardButton = styled(Button)({
|
||||
position: "fixed",
|
||||
right: "20px",
|
||||
bottom: "20px",
|
||||
width: "auto",
|
||||
padding: "0 15px",
|
||||
height: "35px",
|
||||
borderRadius: "30px",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid #464646",
|
||||
color: "#464646",
|
||||
fontFamily: "Fira Sans",
|
||||
fontSize: "16px",
|
||||
lineHeight: "19.2px",
|
||||
fontWeight: 700,
|
||||
transition: "all 0.3s ease-in-out",
|
||||
zIndex: 101,
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
border: `1px solid #d8d8d8`,
|
||||
},
|
||||
});
|
||||
|
||||
export const LeaderboardContainer = styled(Box)(({ theme }) => ({
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: 0,
|
||||
borderLeft: "1px solid #464646",
|
||||
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)",
|
||||
padding: "55px 25px 20px 25px",
|
||||
animation: `${leaderBoardAnimation} 0.5s ease-in-out`,
|
||||
zIndex: 101,
|
||||
overflowY: "auto",
|
||||
}));
|
||||
|
||||
export const LeaderboardColumn = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const LeaderboardRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: "3px",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const LeaderboardText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Inter",
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "16.94px",
|
||||
textAlign: "left",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
justifyContent: "space-between",
|
||||
}));
|
||||
|
||||
export const CloseLeaderboardButton = styled(CloseSVG)({
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
right: "10px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
"&:hover": {
|
||||
transform: "scale(1.05)",
|
||||
},
|
||||
});
|
||||
90
client/src/components/leaderboard/Leaderboard.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { FC } from "react";
|
||||
import {
|
||||
CloseLeaderboardButton,
|
||||
LeaderboardColumn,
|
||||
LeaderboardContainer,
|
||||
LeaderboardRow,
|
||||
LeaderboardText,
|
||||
} from "./Leaderboard-styles";
|
||||
import {
|
||||
LoadingStar,
|
||||
LoadingStarsContainer,
|
||||
} from "../qonnect-four/QonnectFour-Styles";
|
||||
import { StarSVG } from "../common/icons/StarSVG";
|
||||
|
||||
export interface LeaderboardData {
|
||||
numberOfWins: number;
|
||||
winnerQortAddress: string;
|
||||
winnerName?: string;
|
||||
winnerId: string;
|
||||
}
|
||||
|
||||
interface LeaderboardProps {
|
||||
leaderboardData: LeaderboardData[] | null;
|
||||
leaderboardLoading: boolean;
|
||||
setOpenLeaderboard: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const Leaderboard: FC<LeaderboardProps> = ({
|
||||
leaderboardData,
|
||||
leaderboardLoading,
|
||||
setOpenLeaderboard,
|
||||
}) => {
|
||||
if (leaderboardLoading) {
|
||||
return (
|
||||
<LeaderboardContainer>
|
||||
<LoadingStarsContainer>
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<LoadingStar
|
||||
onMouseEnter={() => {
|
||||
return;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
return;
|
||||
}}
|
||||
player={"B"}
|
||||
isWinningCell={true}
|
||||
winner={"true"}
|
||||
onClick={() => {
|
||||
return;
|
||||
}}
|
||||
delay={i * 0.5}
|
||||
key={i}
|
||||
>
|
||||
<StarSVG color="#fff" height={"34"} width={"36"} />
|
||||
</LoadingStar>
|
||||
))}
|
||||
</LoadingStarsContainer>
|
||||
</LeaderboardContainer>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeaderboardContainer>
|
||||
<CloseLeaderboardButton
|
||||
color="#fff"
|
||||
height="28px"
|
||||
width="28px"
|
||||
onClickFunc={() => setOpenLeaderboard(false)}
|
||||
/>
|
||||
<LeaderboardColumn>
|
||||
{leaderboardData &&
|
||||
leaderboardData
|
||||
.sort(
|
||||
(a: LeaderboardData, b: LeaderboardData) =>
|
||||
b.numberOfWins - a.numberOfWins
|
||||
)
|
||||
.map((item: LeaderboardData, index: number) => {
|
||||
return (
|
||||
<LeaderboardRow key={index}>
|
||||
<LeaderboardText>{`${index + 1}. ${
|
||||
item?.winnerName ?? item?.winnerQortAddress
|
||||
}`}</LeaderboardText>
|
||||
<LeaderboardText>{item?.numberOfWins}</LeaderboardText>
|
||||
</LeaderboardRow>
|
||||
);
|
||||
})}
|
||||
</LeaderboardColumn>
|
||||
</LeaderboardContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
746
client/src/components/qonnect-four/QonnectFour-Styles.tsx
Normal file
@@ -0,0 +1,746 @@
|
||||
// Qonnect Four Styles
|
||||
import { Box, Button, CircularProgress, Rating, Typography, keyframes } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { AnimationInfo, PieceColors } from "./QonnectFour";
|
||||
import { VolumeOnSVG } from "../common/icons/VolumeOnSVG";
|
||||
import { VolumeOffSVG } from "../common/icons/VolumeOffSVG";
|
||||
|
||||
interface WinnerProps {
|
||||
winner: string;
|
||||
}
|
||||
|
||||
interface PlayerTurnProps {
|
||||
playerTurn: string;
|
||||
ownUser: boolean;
|
||||
awaitingPayment: boolean;
|
||||
}
|
||||
|
||||
interface AvatarProps {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface CircleProps {
|
||||
time: number;
|
||||
}
|
||||
|
||||
const pulse = keyframes`
|
||||
0% {
|
||||
transform: scale(1);
|
||||
color: red;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
color: orange;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
const winnerModalAnimation = keyframes`
|
||||
0% {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const QonnectFourContainer = styled(Box)<WinnerProps>(
|
||||
({ winner, theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "40px 0px 50px 0px",
|
||||
height: "100vh",
|
||||
gap: "100px",
|
||||
width: "100%",
|
||||
[theme.breakpoints.down("md")]: {
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
height: "auto",
|
||||
gap: "50px",
|
||||
paddingTop: "20px",
|
||||
paddingBottom: "70px",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const QonnectFourBoardContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-evenly",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
export const QonnectFourBoardSubContainer = styled(Box)({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const QonnectFourBoard = styled(Box)({
|
||||
position: "relative",
|
||||
zIndex: 12,
|
||||
width: "669px",
|
||||
height: "auto",
|
||||
padding: "20px",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
gap: "0px",
|
||||
borderRadius: "20px",
|
||||
backgroundColor: "#3F3F3F",
|
||||
});
|
||||
|
||||
export const QonnectFourScoreboard = styled(Box)(({ theme }) => ({
|
||||
display: "none",
|
||||
[theme.breakpoints.down("md")]: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: "26px",
|
||||
height: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
}));
|
||||
|
||||
export const QonnectFourPlayerCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
});
|
||||
|
||||
export const QonnectFourPlayerCard = styled(Box)<PlayerTurnProps>(
|
||||
({ playerTurn, ownUser, awaitingPayment, theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
backgroundColor:
|
||||
playerTurn === "R" || playerTurn === "B" ? "#232428" : "transparent",
|
||||
height: "454px",
|
||||
gap: "37px",
|
||||
...(playerTurn === "R" || playerTurn === "B" || awaitingPayment
|
||||
? {
|
||||
":before": {
|
||||
content: awaitingPayment
|
||||
? '"Awaiting User To Connect..."'
|
||||
: ownUser
|
||||
? '"Your Turn"'
|
||||
: '"Opponent\'s Turn"',
|
||||
position: "absolute",
|
||||
fontFamily: "Inter",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "20px",
|
||||
fontWeight: "700",
|
||||
lineHeight: "24.2px",
|
||||
top: "0px",
|
||||
width: "100%",
|
||||
height: "50px",
|
||||
background:
|
||||
playerTurn === "B"
|
||||
? "linear-gradient(90deg, #232428 3.59%, #70BAFF 50%)"
|
||||
: playerTurn === "R"
|
||||
? "linear-gradient(90deg, #F29999 -50%, #232428 96.41%)"
|
||||
: "transparent",
|
||||
left: "50%",
|
||||
WebkitTransform: "translateX(-50%)",
|
||||
MozTransform: "translateX(-50%)",
|
||||
msTransform: "translateX(-50%)",
|
||||
transform: "translateX(-50%)",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
[theme.breakpoints.down("md")]: {
|
||||
display: "none",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const QonnectFourPlayerCardRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "30px",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const QonnectFourPlayer = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
gap: "37px",
|
||||
});
|
||||
|
||||
export const QonnectFourAvatar = styled("img")({
|
||||
width: "77px",
|
||||
height: "77px",
|
||||
backgroundColor: "#3F3F3F",
|
||||
borderRadius: "50%",
|
||||
objectFit: "cover",
|
||||
});
|
||||
|
||||
export const QonnectFourDefaultAvatar = styled(Box)<AvatarProps>(
|
||||
({ color }) => ({
|
||||
width: "77px",
|
||||
height: "77px",
|
||||
backgroundColor:
|
||||
color === "R" ? "#F29999" : color === "B" ? "#70BAFF" : "#3F3F3F",
|
||||
border:
|
||||
color === "R"
|
||||
? "3px solid #e74545"
|
||||
: color === "B"
|
||||
? "3px solid #2e97fa"
|
||||
: "none",
|
||||
borderRadius: "50%",
|
||||
})
|
||||
);
|
||||
|
||||
export const QonnectFourWrapperCol = styled(Box)<PlayerTurnProps>(
|
||||
({ playerTurn, ownUser, awaitingPayment, theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "3px",
|
||||
backgroundColor:
|
||||
playerTurn === "R" || playerTurn === "B" ? "#232428" : "transparent",
|
||||
padding: "15px",
|
||||
borderRadius: "10px",
|
||||
...(playerTurn === "R" || playerTurn === "B" || awaitingPayment
|
||||
? {
|
||||
":before": {
|
||||
content: awaitingPayment
|
||||
? '"Awaiting User To Connect"'
|
||||
: ownUser
|
||||
? '"Your Turn"'
|
||||
: '"Opponent\'s Turn"',
|
||||
position: "absolute",
|
||||
fontFamily: "Inter",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "20px",
|
||||
fontWeight: "700",
|
||||
lineHeight: "24.2px",
|
||||
bottom: "-40px",
|
||||
width: "100%",
|
||||
height: "50px",
|
||||
background:
|
||||
playerTurn === "B"
|
||||
? "linear-gradient(90deg, #232428 3.59%, #70BAFF 50%)"
|
||||
: playerTurn === "R"
|
||||
? "linear-gradient(90deg, #F29999 -50%, #232428 96.41%)"
|
||||
: "transparent",
|
||||
left: "50%",
|
||||
WebkitTransform: "translateX(-50%)",
|
||||
MozTransform: "translateX(-50%)",
|
||||
msTransform: "translateX(-50%)",
|
||||
transform: "translateX(-50%)",
|
||||
},
|
||||
}
|
||||
: {
|
||||
":before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: "-40px",
|
||||
width: "100%",
|
||||
height: "50px",
|
||||
background: "transparent",
|
||||
left: "50%",
|
||||
WebkitTransform: "translateX(-50%)",
|
||||
MozTransform: "translateX(-50%)",
|
||||
msTransform: "translateX(-50%)",
|
||||
transform: "translateX(-50%)",
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export const QonnectFourPlayerText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Inter",
|
||||
fontSize: "20px",
|
||||
fontWeight: "700",
|
||||
lineHeight: "24.2px",
|
||||
textAlign: "right",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const QonnectFourVSText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "40px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "48.4px",
|
||||
textAlign: "left",
|
||||
alignSelf: "center",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const QonnectFourPlayerTimer = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Inter",
|
||||
fontSize: "20px",
|
||||
fontWeight: "700",
|
||||
lineHeight: "24.2px",
|
||||
textAlign: "right",
|
||||
color: theme.palette.text.primary,
|
||||
padding: "6px 18px 5px 22px",
|
||||
borderRadius: "10px",
|
||||
width: "80px",
|
||||
backgroundColor: "#3F3F3F",
|
||||
userSelect: "none",
|
||||
"&.timer-warning": {
|
||||
animation: `${pulse} 1s infinite`,
|
||||
},
|
||||
}));
|
||||
|
||||
export const QonnectFourHoverRow = styled(Box)({
|
||||
position: "absolute",
|
||||
top: "-60px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const QonnectFourRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
"&:not(:last-child)": {
|
||||
marginBottom: "15px",
|
||||
},
|
||||
"&.hover-row:hover .floatingPiece": {
|
||||
transform: " translateY(10px)",
|
||||
},
|
||||
});
|
||||
|
||||
export const QonnectFourCell = styled(Box)<PieceColors>(
|
||||
({ player, isPlayerTurn, isWinningCell, winner }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "77px",
|
||||
height: "77px",
|
||||
// animation: !isWinningCell ? `${keyframes(dropAnimation)} 0.5s` : "none",
|
||||
background:
|
||||
player === "R"
|
||||
? "#F29999"
|
||||
: player === "B"
|
||||
? "#70BAFF"
|
||||
: player === "TIE"
|
||||
? "linear-gradient(to right, #F29999 0%, #F29999 50%, #70BAFF 50%, #70BAFF 100%)"
|
||||
: "#27282C",
|
||||
borderRadius: "50%",
|
||||
border:
|
||||
player === "R"
|
||||
? "3px solid #F29999"
|
||||
: player === "B"
|
||||
? "3px solid#70BAFF"
|
||||
: player === "TIE"
|
||||
? "none"
|
||||
: "3px solid #272626",
|
||||
boxShadow:
|
||||
isWinningCell && player === "R"
|
||||
? "0px 0px 23.1px 6px #9C4646"
|
||||
: isWinningCell && player === "B"
|
||||
? "0px 0px 23.1px 6px #46739C"
|
||||
: "none",
|
||||
"&:hover": {
|
||||
cursor: (winner || !isPlayerTurn) ? "auto" : "pointer",
|
||||
},
|
||||
"&.floatingPiece": {
|
||||
transition: "transform 0.3s ease-in-out, opacity 0.3s ease-in-out",
|
||||
transform: winner ? "none" : "translateY(5px)",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const WinnerText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "22px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "19.36px",
|
||||
textAlign: "left",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const ResignButtonRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "15px",
|
||||
width: "100%",
|
||||
height: "77px",
|
||||
marginTop: "40px",
|
||||
});
|
||||
|
||||
export const ResignButton = styled(Button)({
|
||||
position: "fixed",
|
||||
left: "20px",
|
||||
bottom: "20px",
|
||||
width: "86px",
|
||||
height: "35px",
|
||||
borderRadius: "30px",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid #464646",
|
||||
color: "#464646",
|
||||
fontFamily: "Fira Sans",
|
||||
fontSize: "16px",
|
||||
lineHeight: "19.2px",
|
||||
fontWeight: 700,
|
||||
transition: "all 0.3s ease-in-out",
|
||||
zIndex: 101,
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
border: `1px solid #d8d8d8`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const AnimatedPiece = styled(Box)<AnimationInfo>(
|
||||
({ player, dropHeight, duration }) => {
|
||||
const animationName = `dropPiece-${Math.random()
|
||||
.toString(36)
|
||||
.substring(7)}`;
|
||||
return {
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
width: "77px",
|
||||
height: "77px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: player === "R" ? "#F29999" : "#70BAFF",
|
||||
transform: `translateY(${dropHeight}px)`,
|
||||
animation: `${animationName} ${duration / 1000}s linear forwards`, // use the unique animation name
|
||||
// Define the keyframes within the same context
|
||||
[`@keyframes ${animationName}`]: {
|
||||
"0%": { transform: "translateY(0)" },
|
||||
"100%": { transform: `translateY(${dropHeight}px)` },
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const WinsCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
});
|
||||
|
||||
export const WinsTypography = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Inter",
|
||||
fontSize: "17px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "24.2px",
|
||||
textAlign: "right",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const StyledWinIcon = styled(Rating)({
|
||||
gap: "5px",
|
||||
});
|
||||
|
||||
export const WinningCard = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
justifyContent: "center",
|
||||
marginBottom: "15px",
|
||||
zIndex: 100,
|
||||
width: "100%",
|
||||
height: "77px",
|
||||
padding: "15px 0",
|
||||
alignSelf: "center",
|
||||
borderRadius: "10px",
|
||||
border: "5px solid #3F3F3F",
|
||||
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)",
|
||||
animation: `${winnerModalAnimation} 0.5s ease-in-out`,
|
||||
}));
|
||||
|
||||
export const WinningCardCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "67px",
|
||||
});
|
||||
|
||||
export const WinningCardSubContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
padding: "0 20px",
|
||||
gap: "5px"
|
||||
});
|
||||
|
||||
export const ButtonRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
gap: "20px",
|
||||
});
|
||||
|
||||
export const PlayAgainButton = styled(Button)({
|
||||
width: "auto",
|
||||
height: "43px",
|
||||
borderRadius: "30px",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid #ffffff",
|
||||
color: "#ffffff",
|
||||
fontFamily: "Fira Sans",
|
||||
fontSize: "16px",
|
||||
lineHeight: "19.2px",
|
||||
fontWeight: 700,
|
||||
padding: "12px 25px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
border: `1px solid #d8d8d8`,
|
||||
},
|
||||
});
|
||||
|
||||
export const WinningCardRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: "9px",
|
||||
});
|
||||
|
||||
export const WinningCardTitleText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "32px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "38.72px",
|
||||
textAlign: "left",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const WinningCardSubTitleText = styled(Typography)<WinnerProps>(
|
||||
({ winner }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "32px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "38.72px",
|
||||
textAlign: "left",
|
||||
color: winner === "B" ? "#70BAFF" : "#F29999",
|
||||
})
|
||||
);
|
||||
|
||||
export const WinningCardButtonCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "15px",
|
||||
});
|
||||
|
||||
export const ResignModalButton = styled(Button)({
|
||||
fontFamily: "Fira Sans",
|
||||
lineHeight: "19.2px",
|
||||
fontSize: "16px",
|
||||
fontWeight: "600",
|
||||
width: "auto",
|
||||
height: "43px",
|
||||
padding: "5px 24px",
|
||||
borderRadius: "30px",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid #464646",
|
||||
color: "#464646",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
border: `1px solid #d8d8d8`,
|
||||
},
|
||||
});
|
||||
|
||||
export const MuteButton = styled(VolumeOnSVG)({
|
||||
position: "fixed",
|
||||
top: "15px",
|
||||
right: "20px",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const UnmuteButton = styled(VolumeOffSVG)({
|
||||
position: "fixed",
|
||||
top: "15px",
|
||||
right: "20px",
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
// Loading modal
|
||||
|
||||
// Animation keyframes
|
||||
const fadeIn = keyframes`
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
20%, 80% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
`;
|
||||
|
||||
export const LoadingStarsContainer = styled(Box)({
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
});
|
||||
|
||||
export const LoadingModalText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "32px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "38.72px",
|
||||
textAlign: "left",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const LoadingStar = styled(QonnectFourCell)<PieceColors>(
|
||||
({ delay }) => ({
|
||||
opacity: 0, // Start invisible
|
||||
animation: `${fadeIn} 4.5s ease infinite`,
|
||||
animationDelay: `${delay}s`,
|
||||
})
|
||||
);
|
||||
|
||||
export const BestOfFontCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "5px",
|
||||
marginTop: "40px",
|
||||
userSelect: "none",
|
||||
});
|
||||
|
||||
export const BestOfFont = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fira Sans",
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "19.2px",
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
// Opponent Found Modal
|
||||
|
||||
export const OpponentFoundText = styled(Typography)({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "32px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "38.72px",
|
||||
color: "#0085FF",
|
||||
});
|
||||
|
||||
export const AwaitingPaymentText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "22px",
|
||||
fontWeight: 700,
|
||||
lineHeight: "24.2px",
|
||||
textAlign: "left",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const OpponentLeftText = styled(Typography)({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "32px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "24.2px",
|
||||
textAlign: "left",
|
||||
color: "#F29999",
|
||||
});
|
||||
|
||||
export const PlayersConnectedCard = styled(Box)(({ theme }) => ({
|
||||
position: "absolute",
|
||||
top: "-12px",
|
||||
display: "flex",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
justifyContent: "center",
|
||||
marginBottom: "15px",
|
||||
zIndex: 100,
|
||||
width: "auto",
|
||||
height: "100px",
|
||||
padding: "15px 0",
|
||||
alignSelf: "center",
|
||||
borderRadius: "10px",
|
||||
border: "5px solid #3F3F3F",
|
||||
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)",
|
||||
animation: `${winnerModalAnimation} 0.5s ease-in-out`,
|
||||
}));
|
||||
|
||||
export const PlayersConnectedText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "30px",
|
||||
textAlign: "center",
|
||||
fontWeight: 600,
|
||||
lineHeight: "24.2px",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const ModalCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "25px",
|
||||
justifyContent: "center",
|
||||
margin: "25px 0 0 0",
|
||||
});
|
||||
|
||||
export const CustomCircularProgress = styled(CircularProgress)<CircleProps>(({ time, theme }) => ({
|
||||
position: "relative",
|
||||
color: "#0085FF",
|
||||
"&::before": {
|
||||
content: `"${time}"`,
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "30px",
|
||||
color: theme.palette.text.primary,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
width: "calc(100% - 2px)",
|
||||
height: "calc(100% - 2px)",
|
||||
borderRadius: "50%",
|
||||
background:`radial-gradient(circle at center, transparent 34%, #fffffff0 34%)`,
|
||||
transform: "translate(-50%, -50%) rotate(90deg)",
|
||||
zIndex: -1,
|
||||
},
|
||||
|
||||
"& .MuiCircularProgress-circle": {
|
||||
zIndex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
export const PaymentTimerText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fira Sans",
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
fontSize: "18px",
|
||||
lineHeight: "17px",
|
||||
textAlign: "center",
|
||||
userSelect: "none",
|
||||
}));
|
||||
2008
client/src/components/qonnect-four/QonnectFour.tsx
Normal file
246
client/src/components/slider/CustomSlider.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { FC, useContext, useState } from "react";
|
||||
import ReactGA from "react-ga4";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CustomLoader,
|
||||
DoubleCaretRightIcon,
|
||||
DoubleCaretRightIcon2,
|
||||
DoubleCaretRightIcon3,
|
||||
InfoBox,
|
||||
InfoBoxCol,
|
||||
InfoBoxError,
|
||||
InfoBoxHighlightedText,
|
||||
InfoBoxRow,
|
||||
InfoBoxText,
|
||||
InfoBoxTextInnerCol,
|
||||
InfoIcon,
|
||||
SliderContainer,
|
||||
SliderRow,
|
||||
SliderText,
|
||||
StyledSlider,
|
||||
TimesIcon,
|
||||
} from "./Slider-styles";
|
||||
import gameService from "../../services/gameService";
|
||||
import socketService from "../../services/socketService";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
import SlideUnlockSound from "../../assets/audio/SlideUnlockSound.mp3";
|
||||
import { NotificationContext } from "../../contexts/notificationContext";
|
||||
import { verifyBalance } from "../../utils/verifyBalance";
|
||||
import { LoadingContext } from "../../contexts/loadingContext";
|
||||
|
||||
interface CustomSliderProps {
|
||||
preventPlaying: boolean;
|
||||
}
|
||||
|
||||
export const CustomSlider: FC<CustomSliderProps> = ({ preventPlaying }) => {
|
||||
const [sliderValue, setSliderValue] = useState<number>(0);
|
||||
const [displayInfoBox, setDisplayInfoBox] = useState<boolean>(false);
|
||||
const [displayInfoError, setDisplayInfoError] = useState<boolean>(false);
|
||||
|
||||
const { userInfo } = useContext(gameContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { setNotification } = useContext(NotificationContext);
|
||||
const { loadingSlider, setLoadingSlider, setLoadingGame } =
|
||||
useContext(LoadingContext);
|
||||
|
||||
const handleSlideChange = async (event: any, value: number | number[]) => {
|
||||
if (preventPlaying) {
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "You are banned from playing for 24 hours because you failed to make a payment.",
|
||||
});
|
||||
setDisplayInfoError(true);
|
||||
setDisplayInfoBox(false);
|
||||
setSliderValue(0);
|
||||
return;
|
||||
}
|
||||
if (loadingSlider) {
|
||||
setSliderValue(0);
|
||||
return;
|
||||
}
|
||||
const audio = new Audio(SlideUnlockSound);
|
||||
const newValue = typeof value === "number" ? value : value[0];
|
||||
if (sliderValue < 95) {
|
||||
setSliderValue(0);
|
||||
} else {
|
||||
// CHANGE THIS LOGIC IN PROD TO CHECK IF USER IS CONNECTED
|
||||
setLoadingSlider(true);
|
||||
setSliderValue(newValue as number);
|
||||
const userAddress = userInfo?.address;
|
||||
console.log({ userAddress });
|
||||
if (!userAddress) {
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "Please connect your wallet",
|
||||
});
|
||||
setDisplayInfoError(true);
|
||||
setDisplayInfoBox(false);
|
||||
setLoadingSlider(false);
|
||||
setTimeout(() => {
|
||||
setSliderValue(0);
|
||||
return;
|
||||
}, 2000);
|
||||
}
|
||||
// Check if user has enough balance to play
|
||||
const hasBalance = await verifyBalance(
|
||||
userInfo,
|
||||
setNotification,
|
||||
setLoadingSlider
|
||||
);
|
||||
if (!hasBalance) {
|
||||
setDisplayInfoError(true);
|
||||
setDisplayInfoBox(false);
|
||||
ReactGA.event({
|
||||
category: "Insufficient Balance Error",
|
||||
action: "Slid the slider but without enough balance",
|
||||
label: "Slid the slider but without enough balance",
|
||||
});
|
||||
setTimeout(() => {
|
||||
setSliderValue(0);
|
||||
return;
|
||||
}, 2000);
|
||||
return
|
||||
}
|
||||
const socket = socketService.socket;
|
||||
if (!socket) return;
|
||||
const game = await gameService
|
||||
.generateGame(socket, userAddress)
|
||||
.catch((err) => {
|
||||
setLoadingSlider(false);
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "Error generating game",
|
||||
});
|
||||
console.error(err);
|
||||
});
|
||||
const roomId = game.roomId;
|
||||
ReactGA.event({
|
||||
category: "Game Room Generated",
|
||||
action: "Slid the slider and game room was generated",
|
||||
label: "Slid the slider and game room was generated",
|
||||
});
|
||||
setLoadingSlider(false);
|
||||
setLoadingGame(true);
|
||||
navigate(`/game/${roomId}`);
|
||||
audio.play();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SliderContainer>
|
||||
<SliderRow>
|
||||
{displayInfoError && (
|
||||
<InfoBoxError
|
||||
style={{
|
||||
height: preventPlaying ? "auto" : "59px",
|
||||
top: preventPlaying ? "-90px" : "-80px",
|
||||
}}
|
||||
>
|
||||
{preventPlaying ? (
|
||||
<InfoBoxRow>
|
||||
<InfoBoxText style={{ fontWeight: 600 }}>
|
||||
You are banned from playing for 24 hours because you failed to
|
||||
make a payment.
|
||||
</InfoBoxText>
|
||||
</InfoBoxRow>
|
||||
) : (
|
||||
<InfoBoxRow>
|
||||
<InfoBoxText style={{ fontWeight: 600 }}>
|
||||
Unable to start game. Please check
|
||||
</InfoBoxText>
|
||||
<InfoBoxHighlightedText
|
||||
onClick={() => {
|
||||
setDisplayInfoError(false);
|
||||
setDisplayInfoBox(true);
|
||||
}}
|
||||
>
|
||||
How to play
|
||||
</InfoBoxHighlightedText>
|
||||
</InfoBoxRow>
|
||||
)}
|
||||
<TimesIcon
|
||||
color={"#ffffff"}
|
||||
height="18"
|
||||
width="18"
|
||||
onClickFunc={() => {
|
||||
if (preventPlaying) {
|
||||
setDisplayInfoError(false);
|
||||
return;
|
||||
}
|
||||
setDisplayInfoError(false);
|
||||
setDisplayInfoBox(true);
|
||||
}}
|
||||
/>
|
||||
</InfoBoxError>
|
||||
)}
|
||||
{displayInfoBox && (
|
||||
<InfoBox>
|
||||
<InfoBoxCol>
|
||||
<InfoBoxText style={{ fontWeight: 600 }}>
|
||||
How to play:
|
||||
</InfoBoxText>
|
||||
<InfoBoxTextInnerCol>
|
||||
<InfoBoxRow>
|
||||
<InfoBoxText>1. Install the </InfoBoxText>
|
||||
<InfoBoxHighlightedText
|
||||
href="https://bit.ly/qortal-chrome-extension"
|
||||
aria-label="Visit the chrome store to download the Qortal Chrome Extension"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Qortal Extension
|
||||
</InfoBoxHighlightedText>{" "}
|
||||
</InfoBoxRow>
|
||||
<InfoBoxText>2. Connect within the extension</InfoBoxText>
|
||||
<InfoBoxRow>
|
||||
<InfoBoxText>3.</InfoBoxText>
|
||||
<InfoBoxHighlightedText
|
||||
href="https://www.youtube.com/watch?v=TnDrrbpRCDk"
|
||||
aria-label="Watch a Youtube video explaining how to acquire QORT"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Acquire QORT
|
||||
</InfoBoxHighlightedText>
|
||||
<InfoBoxText>in order to play</InfoBoxText>
|
||||
</InfoBoxRow>
|
||||
<InfoBoxText>4. Play and win QORT!</InfoBoxText>
|
||||
</InfoBoxTextInnerCol>
|
||||
</InfoBoxCol>
|
||||
<TimesIcon
|
||||
color={"#ffffff"}
|
||||
height="18"
|
||||
width="18"
|
||||
onClickFunc={() => setDisplayInfoBox(false)}
|
||||
/>
|
||||
</InfoBox>
|
||||
)}
|
||||
<SliderText>SWIPE TO PLAY</SliderText>
|
||||
{loadingSlider ? (
|
||||
<CustomLoader size="22px" />
|
||||
) : (
|
||||
<InfoIcon
|
||||
onClickFunc={() => setDisplayInfoBox(true)}
|
||||
color={"#464646"}
|
||||
height={"14px"}
|
||||
width={"14px"}
|
||||
/>
|
||||
)}
|
||||
</SliderRow>
|
||||
{/* <SliderDiv> */}
|
||||
<StyledSlider
|
||||
value={sliderValue}
|
||||
onChange={(event, value) =>
|
||||
setSliderValue(typeof value === "number" ? value : value[0])
|
||||
}
|
||||
onChangeCommitted={handleSlideChange}
|
||||
aria-labelledby="continuous-slider"
|
||||
/>
|
||||
<DoubleCaretRightIcon height="38px" width="38px" color="none" />
|
||||
<DoubleCaretRightIcon2 height="38px" width="38px" color="none" />
|
||||
<DoubleCaretRightIcon3 height="38px" width="38px" color="none" />
|
||||
{/* </SliderDiv> */}
|
||||
</SliderContainer>
|
||||
);
|
||||
};
|
||||
180
client/src/components/slider/Slider-styles.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Box, CircularProgress, Slider, Typography, keyframes } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { DoubleCaretRightSVG } from "../common/icons/DoubleCaretRightSVG";
|
||||
import { InfoSVG } from "../common/icons/InfoSVG";
|
||||
import { CloseSVG } from "../common/icons/CloseSVG";
|
||||
|
||||
const doubleCaretRightAnimation = keyframes`
|
||||
0%, 100% {
|
||||
fill: #ffffff05;
|
||||
}
|
||||
50% {
|
||||
fill: #0085ff;
|
||||
}
|
||||
`
|
||||
|
||||
const InfoBoxAnimation = keyframes`
|
||||
0% {
|
||||
transform: translateY(100px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledSlider = styled(Slider)(({ theme }) => ({
|
||||
" & .MuiSlider-thumb": {
|
||||
width: "77px",
|
||||
height: "77px",
|
||||
zIndex: 2,
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
"& .MuiSlider-rail": {
|
||||
width: "378px",
|
||||
height: "87px",
|
||||
background: "#2B2B2B",
|
||||
borderRadius: "50px",
|
||||
boxShadow: "0px 0px 12.8px -1px #1C5A93"
|
||||
},
|
||||
"& .MuiSlider-track": {
|
||||
display: "none",
|
||||
},
|
||||
}));
|
||||
|
||||
export const SliderContainer = styled(Box)({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "42px",
|
||||
width: "378px"
|
||||
});
|
||||
|
||||
export const SliderDiv = styled(Box)({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignSelf: "flex-start",
|
||||
});
|
||||
|
||||
export const SliderRow = styled(Box)({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "10px",
|
||||
});
|
||||
|
||||
export const SliderText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka One",
|
||||
fontSize: "16px",
|
||||
fontWeight: "400",
|
||||
lineHeight: "19.36px",
|
||||
textAlign: "left",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const DoubleCaretRightIcon = styled(DoubleCaretRightSVG)({
|
||||
position: "absolute",
|
||||
top: "55px",
|
||||
left: "60px",
|
||||
userSelect: "none",
|
||||
animation: `${doubleCaretRightAnimation} 6s infinite`,
|
||||
animationDelay: "0s"
|
||||
});
|
||||
|
||||
export const DoubleCaretRightIcon2 = styled(DoubleCaretRightSVG)({
|
||||
position: "absolute",
|
||||
top: "55px",
|
||||
left: "165px",
|
||||
userSelect: "none",
|
||||
animation: `${doubleCaretRightAnimation} 6s infinite`,
|
||||
animationDelay: '2s'
|
||||
});
|
||||
|
||||
export const DoubleCaretRightIcon3 = styled(DoubleCaretRightSVG)({
|
||||
position: "absolute",
|
||||
top: "55px",
|
||||
left: "270px",
|
||||
userSelect: "none",
|
||||
animation: `${doubleCaretRightAnimation} 6s infinite`,
|
||||
animationDelay: '4s'
|
||||
});
|
||||
|
||||
export const InfoIcon = styled(InfoSVG)({
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
export const InfoBox = styled(Box)({
|
||||
padding: "25px",
|
||||
position: "absolute",
|
||||
top: "-190px",
|
||||
width: "292px",
|
||||
height: "auto",
|
||||
gap: "0px",
|
||||
boxShadow: "0px 4.27px 14.93px 0px #00000026",
|
||||
backgroundColor: "#222222",
|
||||
borderRadius: "3px",
|
||||
zIndex: 1,
|
||||
animation: `${InfoBoxAnimation} 0.5s ease-in-out`,
|
||||
});
|
||||
|
||||
export const InfoBoxError = styled(Box)({
|
||||
boxShadow: "0px 4.27px 14.93px 0px #00000026",
|
||||
backgroundColor: "#222222",
|
||||
width: "419px",
|
||||
padding: "20px",
|
||||
position: "absolute",
|
||||
animation: `${InfoBoxAnimation} 0.5s ease-in-out`,
|
||||
});
|
||||
|
||||
export const InfoBoxCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "15px",
|
||||
});
|
||||
|
||||
export const InfoBoxRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "5px",
|
||||
});
|
||||
|
||||
export const InfoBoxTextInnerCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
});
|
||||
|
||||
export const InfoBoxText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fredoka",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "19.36px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const InfoBoxHighlightedText = styled("a")({
|
||||
fontFamily: "Fredoka",
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
lineHeight: "19.36px",
|
||||
color: "#0085FF",
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
"&:focus": {
|
||||
outline: "2px solid #0085FF",
|
||||
}
|
||||
});
|
||||
|
||||
export const TimesIcon = styled(CloseSVG)({
|
||||
cursor: "pointer",
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
right: "10px",
|
||||
});
|
||||
|
||||
export const CustomLoader = styled(CircularProgress)({
|
||||
color: "#0085FF"
|
||||
});
|
||||
2
client/src/constants/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const playingAmount = 0.25;
|
||||
export const homeAddress = 'Qa4cxK73TXWgA8fXs7sK1fp96syCQTSTmQ'
|
||||
105
client/src/contexts/gameContext.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from "react";
|
||||
|
||||
export interface Player {
|
||||
isConnected: boolean;
|
||||
symbol: "R" | "B";
|
||||
socketId: string;
|
||||
mongoId: string;
|
||||
hasWon?: boolean;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
_id: string;
|
||||
qortAddress: string;
|
||||
}
|
||||
|
||||
interface Score {
|
||||
player: {
|
||||
_id: string;
|
||||
qortAddress: string;
|
||||
};
|
||||
score: number;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface Series {
|
||||
totalGames: number;
|
||||
scores: Score[];
|
||||
}
|
||||
|
||||
export interface UserNameAvatar {
|
||||
name: string;
|
||||
avatar: string;
|
||||
symbol: "R" | "B";
|
||||
}
|
||||
|
||||
export interface Game {
|
||||
status: "waiting" | "active" | "finished" | "finished-forfeit";
|
||||
players: User[];
|
||||
winner: {
|
||||
qortAddress: string;
|
||||
_id: string;
|
||||
};
|
||||
playerPayments: {
|
||||
player: {
|
||||
qortAddress: string;
|
||||
_id: string;
|
||||
};
|
||||
payment: string;
|
||||
}[];
|
||||
series: Series;
|
||||
roomId: string;
|
||||
history: {
|
||||
state: [[string]]; // 2D array representing the game's final state
|
||||
winner: {
|
||||
qortAddress: string;
|
||||
_id: string;
|
||||
}; // Game winner
|
||||
startedAt: Date;
|
||||
tie: boolean; // Indicates if the game ended in a tie
|
||||
}[];
|
||||
}
|
||||
export interface IGameContextProps {
|
||||
gameWinner: "R" | "B" | "TIE" | null;
|
||||
setGameWinner: (val: "R" | "B" | "TIE" | null) => void;
|
||||
isInRoom: boolean;
|
||||
setInRoom: (inRoom: boolean) => void;
|
||||
playerSymbol: "R" | "B";
|
||||
setPlayerSymbol: (symbol: "R" | "B") => void;
|
||||
isPlayerTurn: boolean;
|
||||
setPlayerTurn: (turn: boolean) => void;
|
||||
isGameStarted: boolean;
|
||||
setGameStarted: (started: boolean) => void;
|
||||
setPlayers: (players: Record<string, Player>) => void;
|
||||
players: Record<string, Player>;
|
||||
game: Game | null;
|
||||
setGame: (val: Game | null) => void;
|
||||
userInfo: any;
|
||||
setUserInfo: (val: any) => void;
|
||||
userNameAvatar: Record<string, UserNameAvatar>;
|
||||
setUserNameAvatar: (userNameAvatar: Record<string, UserNameAvatar>) => void;
|
||||
}
|
||||
|
||||
const defaultState: IGameContextProps = {
|
||||
gameWinner: null,
|
||||
setGameWinner: () => {},
|
||||
isInRoom: false,
|
||||
setInRoom: () => {},
|
||||
playerSymbol: "R",
|
||||
setPlayerSymbol: () => {},
|
||||
isPlayerTurn: false,
|
||||
setPlayerTurn: () => {},
|
||||
isGameStarted: false,
|
||||
setGameStarted: () => {},
|
||||
players: {},
|
||||
setPlayers: () => {},
|
||||
game: null,
|
||||
setGame: () => {},
|
||||
userInfo: null,
|
||||
setUserInfo: () => {},
|
||||
userNameAvatar: {},
|
||||
setUserNameAvatar: () => {},
|
||||
};
|
||||
|
||||
export default React.createContext(defaultState);
|
||||
15
client/src/contexts/loadingContext.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface LoadingContextProps {
|
||||
loadingGame: boolean;
|
||||
loadingSlider: boolean;
|
||||
setLoadingGame: (loading: boolean) => void;
|
||||
setLoadingSlider: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const LoadingContext = createContext<LoadingContextProps>({
|
||||
loadingSlider: false,
|
||||
setLoadingSlider: () => {},
|
||||
loadingGame: false,
|
||||
setLoadingGame: () => {},
|
||||
});
|
||||
24
client/src/contexts/notificationContext.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
interface AlertTypes {
|
||||
alertSuccess: string;
|
||||
alertError: string;
|
||||
alertInfo: string;
|
||||
}
|
||||
|
||||
export interface NotificationProps {
|
||||
alertType: keyof AlertTypes | '';
|
||||
msg: string;
|
||||
}
|
||||
|
||||
interface NotificationContextProps {
|
||||
notification: NotificationProps;
|
||||
setNotification: (notification: NotificationProps) => void;
|
||||
resetNotification: () => void;
|
||||
}
|
||||
|
||||
export const NotificationContext = createContext<NotificationContextProps>({
|
||||
notification: { alertType: '', msg: ''},
|
||||
setNotification: () => {},
|
||||
resetNotification: () => {},
|
||||
});
|
||||
13
client/src/contexts/userContext.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface UserContextProps {
|
||||
avatar: string;
|
||||
setAvatar: (avatar: string) => void;
|
||||
}
|
||||
|
||||
export const UserContext = createContext<UserContextProps>({
|
||||
avatar: '',
|
||||
setAvatar: () => {},
|
||||
});
|
||||
|
||||
|
||||
49
client/src/index.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Zen+Tokyo+Zoo&display=swap");
|
||||
|
||||
@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;
|
||||
}
|
||||
10
client/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import "./index.scss";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
)
|
||||
64
client/src/pages/Game/Game.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { AppContainer, MainContainer } from "../../App-styles";
|
||||
import { useContext, useEffect, useMemo } from "react";
|
||||
import gameContext from "../../contexts/gameContext";
|
||||
import socketService from "../../services/socketService";
|
||||
import gameService from "../../services/gameService";
|
||||
import { QonnectFour } from "../../components/qonnect-four/QonnectFour";
|
||||
import { LoadingContext } from "../../contexts/loadingContext";
|
||||
import { NotificationContext } from "../../contexts/notificationContext";
|
||||
|
||||
export const GamePage = () => {
|
||||
const { userInfo, setInRoom, setGame } = useContext(gameContext);
|
||||
const { setLoadingGame } = useContext(LoadingContext);
|
||||
const { setNotification } = useContext(NotificationContext);
|
||||
const { id } = useParams();
|
||||
|
||||
const userAddress = useMemo(() => {
|
||||
return userInfo?.address;
|
||||
}, [userInfo]);
|
||||
|
||||
const joinRoom = async (roomId: string, userAddress: string) => {
|
||||
try {
|
||||
if (!userAddress) return;
|
||||
const socket = socketService.socket;
|
||||
if (!roomId || roomId.trim() === "" || !socket) return;
|
||||
setLoadingGame(true);
|
||||
const joined = await gameService
|
||||
.joinGameRoom(socket, roomId, userAddress)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: err.toString(),
|
||||
});
|
||||
});
|
||||
if (joined) {
|
||||
setInRoom(true);
|
||||
if (joined?.game) {
|
||||
setGame(joined.game);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: error.toString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (id && userAddress) {
|
||||
joinRoom(id, userAddress);
|
||||
}
|
||||
}, [id, userAddress]);
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<QonnectFour />
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
122
client/src/pages/Home/Home-Styles.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
export const BubbleBoard = styled(Box)({
|
||||
position: "relative",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(9, 1fr)",
|
||||
gridTemplateRows: "repeat(4, 1fr)",
|
||||
gap: "15px",
|
||||
width: "815px",
|
||||
height: "353px",
|
||||
});
|
||||
|
||||
export const BubbleCard = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "#ffffff05",
|
||||
borderRadius: "50%",
|
||||
fontFamily: "Fredoka One, sans-serif",
|
||||
fontWeight: 500,
|
||||
fontSize: "40px",
|
||||
lineHeight: "48.4px",
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
|
||||
export const BubbleCardColored1 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(124.49deg, #70BAFF 7.03%, #F29999 94.22%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const BubbleCardColored2 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(36.5deg, #70BAFF 19.69%, #F29999 90.73%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const BubbleCardColored3 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(180deg, #70BAFF -24.68%, #ACABD0 25.49%, #F29999 74.03%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const BubbleCardColored4 = styled(Box)({
|
||||
height: "77px",
|
||||
width: "77px",
|
||||
background: "linear-gradient(275.71deg, #70BAFF 35.99%, #F29999 95.61%)",
|
||||
boxShadow: "0px 0px 25.8px -1px #1C5A93",
|
||||
borderRadius: "50%",
|
||||
});
|
||||
|
||||
export const MainCol = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "10px",
|
||||
});
|
||||
|
||||
export const MainRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: "25px",
|
||||
});
|
||||
|
||||
export const PreventPlayingText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Fira Sans",
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
fontSize: "18px",
|
||||
lineHeight: "17px",
|
||||
textAlign: "center",
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
// OAuth Button
|
||||
|
||||
export const OAuthButtonRow = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const OAuthButton = styled(Button)(({ theme }) => ({
|
||||
background: theme.palette.primary.main,
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: "Fira Sans",
|
||||
fontWeight: 600,
|
||||
fontSize: "22px",
|
||||
lineHeight: "17px",
|
||||
padding: "25px 20px",
|
||||
borderRadius: "5px",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
background: theme.palette.primary.dark,
|
||||
},
|
||||
}));
|
||||
|
||||
export const HomeWrapper = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "100px",
|
||||
height: "90vh",
|
||||
width: "100%",
|
||||
});
|
||||
113
client/src/pages/Home/Home.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { FC, useContext, 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 { sendRequestToExtension, serverUrl } from "../../App";
|
||||
|
||||
import { NotificationContext } from "../../contexts/notificationContext";
|
||||
|
||||
import { TradeOffers } from "../../components/Grids/TradeOffers";
|
||||
|
||||
interface IsInstalledReturn {
|
||||
success: boolean;
|
||||
message: string;
|
||||
userInfo: any | null;
|
||||
}
|
||||
interface IsInstalledProps {
|
||||
connectSocket: () => void;
|
||||
isSocketUp: boolean;
|
||||
isInstalledFunc: () => Promise<IsInstalledReturn>;
|
||||
}
|
||||
|
||||
export const HomePage: FC<IsInstalledProps> = ({
|
||||
connectSocket,
|
||||
isSocketUp,
|
||||
isInstalledFunc,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { } = useContext(gameContext);
|
||||
const { setNotification } = useContext(NotificationContext);
|
||||
|
||||
|
||||
const [OAuthLoading, setOAuthLoading] = useState<boolean>(false);
|
||||
|
||||
|
||||
// OAuth logic
|
||||
const oAuthFunc = async () => {
|
||||
setOAuthLoading(true);
|
||||
try {
|
||||
const userInfoResponse = await isInstalledFunc();
|
||||
if (!userInfoResponse.success) {
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: userInfoResponse.message,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
const res = await axios.post(
|
||||
`${serverUrl}/api/game/oauth`,
|
||||
{
|
||||
qortAddress: userInfoResponse.userInfo?.address,
|
||||
publicKey: userInfoResponse.userInfo?.publicKey,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = res.data;
|
||||
const response = await sendRequestToExtension(
|
||||
"REQUEST_OAUTH",
|
||||
{
|
||||
nodeBaseUrl: data.validApi,
|
||||
senderAddress: data.creatorAddress,
|
||||
senderPublicKey: data.senderPublicKey,
|
||||
timestamp: data.timestamp,
|
||||
},
|
||||
300000
|
||||
);
|
||||
const tokenRes = await axios.post(
|
||||
`${serverUrl}/api/game/oauth/verify`,
|
||||
{
|
||||
qortAddress: userInfoResponse.userInfo?.address,
|
||||
code: response,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (tokenRes.data) {
|
||||
localStorage.setItem("token", tokenRes.data);
|
||||
connectSocket();
|
||||
}
|
||||
setNotification({
|
||||
alertType: "alertSuccess",
|
||||
msg: userInfoResponse.message,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNotification({
|
||||
alertType: "alertError",
|
||||
msg: "Error when trying to authenticate, please try again!",
|
||||
});
|
||||
} finally {
|
||||
setOAuthLoading(false);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
{/* <Header /> */}
|
||||
<TradeOffers />
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
135
client/src/services/gameService/index.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Socket } from "socket.io-client";
|
||||
import { Player } from "../../contexts/gameContext";
|
||||
import { Board } from "../../components/qonnect-four/QonnectFour";
|
||||
|
||||
export interface IStartGame {
|
||||
start: boolean;
|
||||
symbol: "R" | "B";
|
||||
}
|
||||
|
||||
class GameService {
|
||||
public async joinGameRoom(
|
||||
socket: Socket,
|
||||
roomId: string,
|
||||
userAddress: string
|
||||
): Promise<any> {
|
||||
return new Promise((rs, rj) => {
|
||||
socket.emit("join_game", { roomId, userAddress });
|
||||
socket.on("room_joined", (message) => {
|
||||
rs(message);
|
||||
});
|
||||
socket.on("room_join_error", ({ error }) => rj(error));
|
||||
});
|
||||
}
|
||||
public async generateGame(socket: Socket, userAddress: string): Promise<any> {
|
||||
return new Promise((rs, rj) => {
|
||||
socket.emit("generate_game", { userAddress });
|
||||
socket.on("on_generate_game_response", (message) => {
|
||||
rs(message);
|
||||
});
|
||||
socket.on("room_join_error", ({ error }) => rj(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async getListOfBannedAddresses(socket: Socket): Promise<any> {
|
||||
return new Promise((rs, rj) => {
|
||||
socket.emit("get_list_banned_addresses");
|
||||
socket.on("on_list_banned_addresses", (message) => {
|
||||
rs(message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async updateGame(
|
||||
socket: Socket,
|
||||
gameMatrix: {
|
||||
column: number;
|
||||
row: number;
|
||||
symbol: "R" | "B";
|
||||
}
|
||||
) {
|
||||
socket.emit("update_game", { matrix: gameMatrix });
|
||||
}
|
||||
|
||||
public async leaveRoom(
|
||||
socket: Socket,
|
||||
roomId: string
|
||||
) {
|
||||
socket.emit("leave_room", { roomId });
|
||||
}
|
||||
|
||||
public async paymentMade(socket: Socket, payment: any) {
|
||||
socket.emit("payment_made", payment);
|
||||
}
|
||||
|
||||
public async onGameUpdate(
|
||||
socket: Socket,
|
||||
listiner: (props: {
|
||||
matrix: Board;
|
||||
currColumns: number[];
|
||||
timeRed: number;
|
||||
timeBlue: number;
|
||||
}) => void
|
||||
) {
|
||||
socket.on("on_game_update", (props) => listiner(props));
|
||||
}
|
||||
|
||||
public async onTurnUpdate(
|
||||
socket: Socket,
|
||||
listiner: (isTurn: boolean) => void
|
||||
) {
|
||||
socket.on("on_turn_update", (isTurn) => listiner(isTurn));
|
||||
}
|
||||
|
||||
public async onSetFullGameData(
|
||||
socket: Socket,
|
||||
listiner: (gameData: any) => void
|
||||
) {
|
||||
socket.on("on_set_full_game_data", (message) => listiner(message));
|
||||
}
|
||||
public async onPaymentNotMade(
|
||||
socket: Socket,
|
||||
listiner: (gameData: any) => void
|
||||
) {
|
||||
socket.on("payment_not_made", (message) => listiner(message));
|
||||
}
|
||||
|
||||
|
||||
public async onStartGame(
|
||||
socket: Socket,
|
||||
listiner: (options: IStartGame) => void
|
||||
) {
|
||||
socket.on("start_game", listiner);
|
||||
}
|
||||
|
||||
public async onGameWin(socket: Socket, listiner: (message: any) => void) {
|
||||
socket.on("on_game_win", (message) => listiner(message));
|
||||
}
|
||||
public async onGameTie(socket: Socket, listiner: (message: any) => void) {
|
||||
socket.on("on_game_tie", (message) => listiner(message));
|
||||
}
|
||||
|
||||
public async onResumeGame(
|
||||
socket: Socket,
|
||||
listiner: (data: {
|
||||
matrix: any[][];
|
||||
symbol: "R" | "B";
|
||||
timeRed: number;
|
||||
timeBlue: number;
|
||||
players: Record<string, Player>;
|
||||
}) => void
|
||||
) {
|
||||
socket.on("on_resume_game", (data) => listiner(data));
|
||||
}
|
||||
|
||||
public async onPlayersInfo(
|
||||
socket: Socket,
|
||||
listiner: (data: Record<string, Player>) => void
|
||||
) {
|
||||
socket.on("on_players_info", (data) => {
|
||||
listiner(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new GameService();
|
||||
42
client/src/services/socketService/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { DefaultEventsMap } from '@socket.io/component-emitter';
|
||||
|
||||
class SocketService {
|
||||
public socket: Socket | null = null;
|
||||
|
||||
public connect(
|
||||
url: string,
|
||||
token: string
|
||||
): Promise<Socket<DefaultEventsMap, DefaultEventsMap>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = io(url, {
|
||||
auth: {
|
||||
token: token
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.socket) return reject("Socket initialization failed");
|
||||
|
||||
this.socket.on("connect", () => {
|
||||
console.log('connected');
|
||||
resolve(this.socket as Socket);
|
||||
});
|
||||
|
||||
this.socket.on("connect_error", (err) => {
|
||||
console.log("Connection error: ", err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null; // Optionally reset the socket to null after disconnecting
|
||||
console.log('Disconnected');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new SocketService();
|
||||
|
||||
160
client/src/styles/theme.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
const commonThemeOptions = createTheme({
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
":root": {
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
html: {
|
||||
scrollBehavior: "smooth",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "inherit",
|
||||
transition: "filter 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
filter: "brightness(1.1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "'Fira Sans', 'Fredoka One', 'Inter'",
|
||||
button: {
|
||||
textTransform: "none",
|
||||
},
|
||||
h1: {
|
||||
fontSize: "42px",
|
||||
},
|
||||
h2: {
|
||||
fontSize: "32px",
|
||||
},
|
||||
h3: {
|
||||
fontSize: "21px",
|
||||
},
|
||||
h4: {
|
||||
fontSize: "18px",
|
||||
},
|
||||
h5: {
|
||||
fontSize: "16px",
|
||||
},
|
||||
h6: {
|
||||
fontSize: "14px",
|
||||
},
|
||||
body1: {
|
||||
fontSize: "1rem",
|
||||
},
|
||||
body2: {
|
||||
fontSize: "0.875rem",
|
||||
},
|
||||
},
|
||||
spacing: 8, // Customize the base spacing unit (default is 8)
|
||||
shape: {
|
||||
borderRadius: 4, // Customize the border radius of components
|
||||
},
|
||||
breakpoints: {
|
||||
values: {
|
||||
xs: 0,
|
||||
sm: 600,
|
||||
md: 1160,
|
||||
lg: 1280,
|
||||
xl: 1920,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const darkTheme = createTheme({
|
||||
...commonThemeOptions,
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: "#0085ff",
|
||||
// dark: "#043596",
|
||||
light: "#70BAFF",
|
||||
},
|
||||
secondary: {
|
||||
main: "#F29999",
|
||||
// light: "#5657b1",
|
||||
// dark: "#302F40"
|
||||
},
|
||||
background: {
|
||||
default: "#27282c",
|
||||
},
|
||||
text: {
|
||||
primary: "#ffffff",
|
||||
secondary: "#464646",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
"body::-webkit-scrollbar-track": {
|
||||
backgroundColor: "#27282c",
|
||||
},
|
||||
"body::-webkit-scrollbar-track:hover": {
|
||||
backgroundColor: "#27282c",
|
||||
},
|
||||
"body::-webkit-scrollbar": {
|
||||
width: "16px",
|
||||
height: "10px",
|
||||
backgroundColor: "#27282c",
|
||||
},
|
||||
"body::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: "#171a27",
|
||||
borderRadius: "8px",
|
||||
backgroundClip: "content-box",
|
||||
border: "4px solid transparent",
|
||||
},
|
||||
"body::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: "#0e1018",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
boxShadow: "none",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
"0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
boxShadow: "none",
|
||||
transition: "filter 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
backgroundColor: "inherit",
|
||||
boxShadow:
|
||||
"0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
disableElevation: true,
|
||||
disableRipple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { darkTheme };
|
||||
8
client/src/utils/cropAddress.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const cropAddress = (string: string = "", range: number = 5) => {
|
||||
const [start, end] = [
|
||||
string?.substring(0, range),
|
||||
string?.substring(string?.length - range, string?.length),
|
||||
//
|
||||
];
|
||||
return start + "..." + end;
|
||||
};
|
||||
34
client/src/utils/findUsableApi.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const apiEndpoints = [
|
||||
"https://api.qortal.org",
|
||||
"https://api2.qortal.org",
|
||||
"https://appnode.qortal.org",
|
||||
"https://apinode.qortalnodes.live",
|
||||
"https://apinode1.qortalnodes.live",
|
||||
"https://apinode2.qortalnodes.live",
|
||||
"https://apinode3.qortalnodes.live",
|
||||
"https://apinode4.qortalnodes.live",
|
||||
];
|
||||
|
||||
export const findUsableApi = async () => {
|
||||
for (const endpoint of apiEndpoints) {
|
||||
try {
|
||||
const response = await axios.get(`${endpoint}/admin/status`, { timeout: 3000 });
|
||||
const data = response.data;
|
||||
if (data.isSynchronizing === false && data.syncPercent === 100) {
|
||||
return endpoint;
|
||||
} else {
|
||||
console.log(`API not ready: ${endpoint}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.log(`Timeout reached for API ${endpoint}`);
|
||||
} else {
|
||||
console.error(`Error checking API ${endpoint}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No usable API found");
|
||||
}
|
||||
6
client/src/utils/formatTime.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function formatTime(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
// Pad the seconds with a leading zero if less than 10
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
34
client/src/utils/getPlayerNameBySymbol.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { UserNameAvatar } from "../contexts/gameContext";
|
||||
|
||||
export const getPlayerInfoBySymbol = (
|
||||
userNameAvatar: Record<string, UserNameAvatar>,
|
||||
userInfoAddress: string,
|
||||
symbol: string
|
||||
): {
|
||||
name: string;
|
||||
avatar: string;
|
||||
ownUser: boolean;
|
||||
symbol: string;
|
||||
address?: string;
|
||||
} | undefined => {
|
||||
if (userNameAvatar && Object.keys(userNameAvatar).length > 0) {
|
||||
const playerEntry = Object.entries(userNameAvatar).find(
|
||||
([, player]) => player.symbol === symbol
|
||||
);
|
||||
|
||||
if (!playerEntry) {
|
||||
if (symbol === "R") return { name: "Player Red", avatar: "", ownUser: false, symbol: "R"};
|
||||
if (symbol === "B") return { name: "Player Blue", avatar: "", ownUser: false, symbol: "B"};
|
||||
}
|
||||
|
||||
const [playerAddress, playerInfo] = playerEntry ?? [];
|
||||
|
||||
return {
|
||||
name: playerInfo?.name || "Unknown Player",
|
||||
avatar: playerInfo?.avatar || "",
|
||||
ownUser: playerAddress === userInfoAddress,
|
||||
symbol: symbol,
|
||||
address: playerAddress,
|
||||
};
|
||||
}
|
||||
};
|
||||
31
client/src/utils/getPlayerWinCount.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Game, Player } from "../contexts/gameContext";
|
||||
|
||||
interface GameSummary {
|
||||
redWins: number;
|
||||
blueWins: number;
|
||||
}
|
||||
|
||||
export const getPlayerWinCount = (game: Game, players: Record<string, Player>): GameSummary | undefined => {
|
||||
if (game && Object.keys(players).length > 1 && game.series.scores.length > 1) {
|
||||
let redWins = 0;
|
||||
let blueWins = 0;
|
||||
|
||||
game.series.scores.forEach((scoreEntry) => {
|
||||
const playerAddress: string = scoreEntry.player.qortAddress;
|
||||
const player: Player = players[playerAddress];
|
||||
|
||||
if (player.symbol === "R") {
|
||||
redWins += scoreEntry.score;
|
||||
} else if (player.symbol === "B") {
|
||||
blueWins += scoreEntry.score;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
redWins,
|
||||
blueWins,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
45
client/src/utils/verifyBalance.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NotificationProps } from "../contexts/notificationContext";
|
||||
import { findUsableApi } from "../utils/findUsableApi";
|
||||
|
||||
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 validApi: string = await findUsableApi();
|
||||
const balanceUrl: string = `${validApi}/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
client/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
25
client/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
client/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
client/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
16
package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "game-1",
|
||||
"version": "1.0.0",
|
||||
"description": "A full-stack application with React and Express",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"install-client": "cd client && npm install",
|
||||
"build-client": "cd client && npm run build",
|
||||
"install-server": "cd server && npm install",
|
||||
"start-server": "cd server && node index.js",
|
||||
"postinstall": "npm run install-client && npm run build-client && npm run install-server",
|
||||
"start": "npm run start-server"
|
||||
},
|
||||
"author": ""
|
||||
}
|
||||
|
||||
418
server/api/game.js
Normal file
@@ -0,0 +1,418 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { check, validationResult } = require("express-validator");
|
||||
const Game = require("../models/Game");
|
||||
const User = require("../models/User");
|
||||
const ShortUniqueId = require("short-unique-id");
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const {Sha256} = require("asmcrypto.js")
|
||||
|
||||
const moment = require('moment'); // using moment.js to handle dates easily
|
||||
const { findUsableApi } = require("../utils");
|
||||
const { transaction, signChat, base58ToUint8Array, createKeyPair, createTransaction } = require("../transactions/transactions");
|
||||
const { Base58 } = require("../deps/Base58");
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const readFile = util.promisify(fs.readFile); // Promisify readFile for use with async/await
|
||||
const initialBrk = 512 * 1024;
|
||||
let brk = 512 * 1024; // Initialize brk outside to maintain state
|
||||
const waitingQueue = [];
|
||||
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 });
|
||||
const heap = new Uint8Array(memory.buffer);
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
let oauthSecretKeys = {};
|
||||
|
||||
const generateToken = ({user}) => {
|
||||
return jwt.sign({ id: user.id }, process.env.TOKEN_SECRET_KEY, { expiresIn: '750h' }); // 1 month
|
||||
};
|
||||
|
||||
const storageProxy = new Proxy(oauthSecretKeys, {
|
||||
get(target, key, receiver) {
|
||||
cleanupOldEntries(); // Clean up before accessing any key
|
||||
return Reflect.get(target, key, receiver); // Proceed with the default get operation
|
||||
},
|
||||
set(target, key, value, receiver) {
|
||||
value.timestamp = Date.now(); // Add a timestamp to each item when it's added
|
||||
return Reflect.set(target, key, value, receiver); // Proceed with the default set operation
|
||||
}
|
||||
});
|
||||
function cleanupOldEntries() {
|
||||
const tenMinutesAgo = Date.now() - 10 * 60 * 1000; // 10 minutes in milliseconds
|
||||
Object.keys(oauthSecretKeys).forEach(key => {
|
||||
if (oauthSecretKeys[key].timestamp < tenMinutesAgo) {
|
||||
delete oauthSecretKeys[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
function sbrk(size) {
|
||||
const oldBrk = brk;
|
||||
if (brk + size > heap.length) { // Check if the heap can accommodate the request
|
||||
console.log('Not enough memory available, adding to waiting queue');
|
||||
return null; // Not enough memory, return null
|
||||
}
|
||||
brk += size; // Advance brk by the size of the requested memory
|
||||
return oldBrk; // Return the old break point (start of the newly allocated block)
|
||||
}
|
||||
|
||||
function processWaitingQueue() {
|
||||
console.log('Processing waiting queue...');
|
||||
let i = 0;
|
||||
while (i < waitingQueue.length) {
|
||||
const request = waitingQueue[i];
|
||||
const ptr = sbrk(request.size);
|
||||
if (ptr !== null) { // Check if memory was successfully allocated
|
||||
request.resolve(ptr);
|
||||
waitingQueue.splice(i, 1); // Remove the processed request
|
||||
} else {
|
||||
i++; // Continue if the current request cannot be processed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestMemory(size) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ptr = sbrk(size);
|
||||
if (ptr !== null) {
|
||||
resolve(ptr);
|
||||
} else {
|
||||
waitingQueue.push({ size, resolve, reject }); // Add to queue if not enough memory
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetMemory() {
|
||||
brk = initialBrk; // Reset the break point
|
||||
processWaitingQueue(); // Try to process any waiting memory requests
|
||||
}
|
||||
|
||||
const processTransactionVersion2 = async (bytes, validApi) => {
|
||||
try {
|
||||
const response = await axios.post(`${validApi}/transactions/process?apiVersion=2`, Base58.encode(bytes));
|
||||
|
||||
return response.data; // Return the response data from the server
|
||||
} catch (error) {
|
||||
console.error('Error processing transaction:', error.message);
|
||||
throw error; // Rethrow the error for further handling
|
||||
}
|
||||
};
|
||||
|
||||
async function signChatFunc(chatBytesArray, chatNonce, validApi ){
|
||||
let response
|
||||
try {
|
||||
const privateKey = base58ToUint8Array(process.env.KEY_PAIR_PRIVATE);
|
||||
const signedChatBytes = signChat(
|
||||
chatBytesArray,
|
||||
chatNonce,
|
||||
privateKey
|
||||
)
|
||||
|
||||
const res = await processTransactionVersion2(signedChatBytes, validApi)
|
||||
response = res
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e.message)
|
||||
response = false
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
async function loadWebAssembly(memory) {
|
||||
const importObject = {
|
||||
env: {
|
||||
memory: memory // Pass the WebAssembly.Memory object to the module
|
||||
}
|
||||
};
|
||||
|
||||
// Correct the path to point to the specific location of the .wasm file
|
||||
const filename = path.join(__dirname, '../memory-pow/memory-pow.wasm.full');
|
||||
|
||||
try {
|
||||
// Read the .wasm file from the filesystem
|
||||
const buffer = await readFile(filename);
|
||||
const module = await WebAssembly.compile(buffer);
|
||||
|
||||
// Create the WebAssembly instance with the compiled module and import object
|
||||
const instance = new WebAssembly.Instance(module, importObject);
|
||||
|
||||
return instance; // Return the instance to be used elsewhere in your application
|
||||
} catch (error) {
|
||||
console.error('Error loading WebAssembly module:', error);
|
||||
throw error; // Rethrow the error for further handling
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const computePow = async (memory, hashPtr, workBufferPtr, workBufferLength, difficulty) => {
|
||||
|
||||
let response = null
|
||||
|
||||
await new Promise((resolve, reject)=> {
|
||||
|
||||
|
||||
loadWebAssembly(memory)
|
||||
.then(wasmModule => {
|
||||
response = wasmModule.exports.compute2(hashPtr, workBufferPtr, workBufferLength, difficulty)
|
||||
|
||||
resolve()
|
||||
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
module.exports = function (io) {
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const games = await Game.find()
|
||||
res.json('hello');
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).json({
|
||||
errors: [
|
||||
{ msg: "Server error. Please try again or refresh the page." },
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
router.get("/weeklystanding", async (req, res) => {
|
||||
// For example, to get rankings for the current week:
|
||||
const startOfWeek = moment().startOf('week').toDate();
|
||||
const endOfWeek = moment().endOf('week').toDate();
|
||||
try {
|
||||
Game.aggregate([
|
||||
{
|
||||
$match: {
|
||||
createdAt: { $gte: startOfWeek, $lte: endOfWeek }, // Filter games within the week
|
||||
winner: { $ne: null } // Consider games where there is a winner
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$winner", // Group by winner
|
||||
count: { $sum: 1 } // Count the number of wins
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "users", // Assuming the User collection is named 'users'
|
||||
localField: "_id",
|
||||
foreignField: "_id",
|
||||
as: "winnerDetails"
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: "$winnerDetails" // Flatten the winnerDetails array
|
||||
},
|
||||
{
|
||||
$sort: { count: -1 } // Sort by the number of wins descending
|
||||
},
|
||||
{
|
||||
$limit: 50 // Limit to the top 50 results
|
||||
},
|
||||
{
|
||||
$project: { // Structure the output to include the qortAddress
|
||||
_id: 0,
|
||||
winnerId: "$_id",
|
||||
winnerQortAddress: "$winnerDetails.qortAddress", // Assuming 'qortAddress' is a field in the User model
|
||||
winnerName: "$winnerDetails.name", // Include the name field
|
||||
numberOfWins: "$count"
|
||||
}
|
||||
}
|
||||
]).then(results => {
|
||||
res.json(results)
|
||||
}).catch(err => {
|
||||
console.error(err); // Handle possible errors
|
||||
res.status(500).json({
|
||||
errors: [
|
||||
{ msg: "Server error. Please try again or refresh the page." },
|
||||
],
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).json({
|
||||
errors: [
|
||||
{ msg: "Server error. Please try again or refresh the page." },
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/oauth", async (req, res) => {
|
||||
|
||||
try {
|
||||
const validApi = await findUsableApi();
|
||||
|
||||
const qortAddress = req.body.qortAddress
|
||||
const publickey = req.body.publicKey
|
||||
const id = uid.rnd();
|
||||
storageProxy[qortAddress] = { secret: id };
|
||||
|
||||
const recipientPublicKey = publickey
|
||||
let _reference = crypto.randomBytes(64);
|
||||
|
||||
let sendTimestamp = Date.now()
|
||||
|
||||
let reference = Base58.encode(_reference)
|
||||
const keyPair = createKeyPair();
|
||||
const {chatBytes} = await createTransaction(
|
||||
18,
|
||||
keyPair,
|
||||
{
|
||||
timestamp: sendTimestamp,
|
||||
recipient: qortAddress,
|
||||
recipientPublicKey: recipientPublicKey,
|
||||
hasChatReference: 0,
|
||||
message: id,
|
||||
lastReference: reference,
|
||||
proofOfWorkNonce: 0,
|
||||
isEncrypted: 1,
|
||||
isText: 1
|
||||
},
|
||||
|
||||
)
|
||||
|
||||
const _chatBytesArray = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; });
|
||||
const chatBytesArray = new Uint8Array(_chatBytesArray)
|
||||
const chatBytesHash = new Sha256().process(chatBytesArray).finish().result
|
||||
const hashPtr = sbrk(32, heap);
|
||||
const hashAry = new Uint8Array(memory.buffer, hashPtr, 32);
|
||||
hashAry.set(chatBytesHash);
|
||||
|
||||
const difficulty = 8;
|
||||
|
||||
const workBufferLength = 8 * 1024 * 1024;
|
||||
// const workBufferPtr = sbrk(workBufferLength, heap);
|
||||
const workBufferPtr = await requestMemory(workBufferLength);
|
||||
|
||||
let nonce = await computePow(memory, hashPtr, workBufferPtr, workBufferLength, difficulty)
|
||||
brk = initialBrk;
|
||||
|
||||
let _response = await signChatFunc(chatBytesArray,
|
||||
nonce, validApi
|
||||
)
|
||||
res.json({..._response, validApi});
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).json({
|
||||
errors: [
|
||||
{ msg: "Server error. Please try again or refresh the page." },
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
resetMemory()
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/oauth/verify", async (req, res) => {
|
||||
|
||||
try {
|
||||
const {qortAddress, code} = req.body
|
||||
const {secret} = storageProxy[qortAddress]
|
||||
if(code === secret){
|
||||
const token = generateToken({
|
||||
user: {
|
||||
id: qortAddress
|
||||
}
|
||||
})
|
||||
res.json(token)
|
||||
} else {
|
||||
res.json(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
res.status(500).json({
|
||||
errors: [
|
||||
{ msg: "Server error. Please try again or refresh the page." },
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/standingbetween", async (req, res) => {
|
||||
const { before, after } = req.query;
|
||||
|
||||
// Create an object to hold the match criteria for createdAt
|
||||
let matchCriteria = { winner: { $ne: null } }; // Ensure there is a winner
|
||||
|
||||
// Conditionally add date filters based on provided query parameters
|
||||
if (after) {
|
||||
const startDate = new Date(parseInt(after, 10));
|
||||
if (isNaN(startDate.getTime())) {
|
||||
return res.status(400).json({ errors: [{ msg: "Invalid 'after' date format." }] });
|
||||
}
|
||||
matchCriteria.createdAt = matchCriteria.createdAt || {};
|
||||
matchCriteria.createdAt.$gte = startDate;
|
||||
}
|
||||
if (before) {
|
||||
const endDate = new Date(parseInt(before, 10));
|
||||
if (isNaN(endDate.getTime())) {
|
||||
return res.status(400).json({ errors: [{ msg: "Invalid 'before' date format." }] });
|
||||
}
|
||||
matchCriteria.createdAt = matchCriteria.createdAt || {};
|
||||
matchCriteria.createdAt.$lte = endDate;
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Game.aggregate([
|
||||
{
|
||||
$match: matchCriteria
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$winner",
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "users",
|
||||
localField: "_id",
|
||||
foreignField: "_id",
|
||||
as: "winnerDetails"
|
||||
}
|
||||
},
|
||||
{
|
||||
$unwind: "$winnerDetails"
|
||||
},
|
||||
{
|
||||
$sort: { count: -1 }
|
||||
},
|
||||
{
|
||||
$limit: 50
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
winnerId: "$_id",
|
||||
winnerQortAddress: "$winnerDetails.qortAddress",
|
||||
winnerName: "$winnerDetails.name",
|
||||
numberOfWins: "$count"
|
||||
}
|
||||
}
|
||||
]);
|
||||
res.json(results);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({
|
||||
errors: [{ msg: "Server error. Please try again or refresh the page." }]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return router;
|
||||
|
||||
}
|
||||
50
server/constants/index.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const playingAmount = 0.25;
|
||||
const homeAddress = 'Qa4cxK73TXWgA8fXs7sK1fp96syCQTSTmQ'
|
||||
const walletVersion = 2;
|
||||
const TX_TYPES = {
|
||||
1: "Genesis",
|
||||
2: "Payment",
|
||||
3: "Name registration",
|
||||
4: "Name update",
|
||||
5: "Sell name",
|
||||
6: "Cancel sell name",
|
||||
7: "Buy name",
|
||||
8: "Create poll",
|
||||
9: "Vote in poll",
|
||||
10: "Arbitrary",
|
||||
11: "Issue asset",
|
||||
12: "Transfer asset",
|
||||
13: "Create asset order",
|
||||
14: "Cancel asset order",
|
||||
15: "Multi-payment transaction",
|
||||
16: "Deploy AT",
|
||||
17: "Message",
|
||||
18: "Chat",
|
||||
19: "Publicize",
|
||||
20: "Airdrop",
|
||||
21: "AT",
|
||||
22: "Create group",
|
||||
23: "Update group",
|
||||
24: "Add group admin",
|
||||
25: "Remove group admin",
|
||||
26: "Group ban",
|
||||
27: "Cancel group ban",
|
||||
28: "Group kick",
|
||||
29: "Group invite",
|
||||
30: "Cancel group invite",
|
||||
31: "Join group",
|
||||
32: "Leave group",
|
||||
33: "Group approval",
|
||||
34: "Set group",
|
||||
35: "Update asset",
|
||||
36: "Account flags",
|
||||
37: "Enable forging",
|
||||
38: "Reward share",
|
||||
39: "Account level",
|
||||
40: "Transfer privs",
|
||||
41: "Presence"
|
||||
}
|
||||
const QORT_DECIMALS = 1e8
|
||||
const CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP = 1674316800000
|
||||
|
||||
module.exports = { playingAmount, homeAddress, walletVersion, TX_TYPES, QORT_DECIMALS, CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP };
|
||||
13
server/db.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGO_URI);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
// Exit process with failure
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDB;
|
||||
103
server/deps/Base58.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// @ts-nocheck
|
||||
|
||||
const Base58 = {};
|
||||
|
||||
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
const ALPHABET_MAP = {};
|
||||
|
||||
let i = 0;
|
||||
|
||||
while (i < ALPHABET.length) {
|
||||
ALPHABET_MAP[ALPHABET.charAt(i)] = i;
|
||||
i++;
|
||||
}
|
||||
|
||||
Base58.encode = function(buffer) {
|
||||
buffer = new Uint8Array(buffer);
|
||||
var carry, digits, j;
|
||||
if (buffer.length === 0) {
|
||||
return "";
|
||||
}
|
||||
i = void 0;
|
||||
j = void 0;
|
||||
digits = [0];
|
||||
i = 0;
|
||||
while (i < buffer.length) {
|
||||
j = 0;
|
||||
while (j < digits.length) {
|
||||
digits[j] <<= 8;
|
||||
j++;
|
||||
}
|
||||
digits[0] += buffer[i];
|
||||
carry = 0;
|
||||
j = 0;
|
||||
while (j < digits.length) {
|
||||
digits[j] += carry;
|
||||
carry = (digits[j] / 58) | 0;
|
||||
digits[j] %= 58;
|
||||
++j;
|
||||
}
|
||||
while (carry) {
|
||||
digits.push(carry % 58);
|
||||
carry = (carry / 58) | 0;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
i = 0;
|
||||
while (buffer[i] === 0 && i < buffer.length - 1) {
|
||||
digits.push(0);
|
||||
i++;
|
||||
}
|
||||
return digits.reverse().map(function(digit) {
|
||||
return ALPHABET[digit];
|
||||
}).join("");
|
||||
};
|
||||
|
||||
Base58.decode = function(string) {
|
||||
var bytes, c, carry, j;
|
||||
if (string.length === 0) {
|
||||
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(0);
|
||||
}
|
||||
i = void 0;
|
||||
j = void 0;
|
||||
bytes = [0];
|
||||
i = 0;
|
||||
while (i < string.length) {
|
||||
c = string[i];
|
||||
if (!(c in ALPHABET_MAP)) {
|
||||
throw "Base58.decode received unacceptable input. Character '" + c + "' is not in the Base58 alphabet.";
|
||||
}
|
||||
j = 0;
|
||||
while (j < bytes.length) {
|
||||
bytes[j] *= 58;
|
||||
j++;
|
||||
}
|
||||
bytes[0] += ALPHABET_MAP[c];
|
||||
carry = 0;
|
||||
j = 0;
|
||||
while (j < bytes.length) {
|
||||
bytes[j] += carry;
|
||||
carry = bytes[j] >> 8;
|
||||
bytes[j] &= 0xff;
|
||||
++j;
|
||||
}
|
||||
while (carry) {
|
||||
bytes.push(carry & 0xff);
|
||||
carry >>= 8;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
i = 0;
|
||||
while (string[i] === "1" && i < string.length - 1) {
|
||||
bytes.push(0);
|
||||
i++;
|
||||
}
|
||||
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(bytes.reverse());
|
||||
};
|
||||
|
||||
|
||||
// == Changed for ES6 modules == //
|
||||
//}).call(this);
|
||||
|
||||
module.exports = { Base58 };
|
||||
285
server/deps/ed2curve.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* ed2curve: convert Ed25519 signing key pair into Curve25519
|
||||
* key pair suitable for Diffie-Hellman key exchange.
|
||||
*
|
||||
* Written by Dmitry Chestnykh in 2014. Public domain.
|
||||
*/
|
||||
/* jshint newcap: false */
|
||||
|
||||
const { nacl } = require("./nacl-fast");
|
||||
|
||||
|
||||
/*
|
||||
Change to es6 import/export
|
||||
*/
|
||||
|
||||
// (function(root, f) {
|
||||
// 'use strict';
|
||||
// if (typeof module !== 'undefined' && module.exports) module.exports = f(require('tweetnacl'));
|
||||
// else root.ed2curve = f(root.nacl);
|
||||
// }(this, function(nacl) {
|
||||
// 'use strict';
|
||||
// if (!nacl) throw new Error('tweetnacl not loaded');
|
||||
|
||||
// -- Operations copied from TweetNaCl.js. --
|
||||
|
||||
var gf = function (init) {
|
||||
var i,
|
||||
r = new Float64Array(16);
|
||||
if (init) for (i = 0; i < init.length; i++) r[i] = init[i];
|
||||
return r;
|
||||
};
|
||||
|
||||
var gf0 = gf(),
|
||||
gf1 = gf([1]),
|
||||
D = gf([
|
||||
0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898,
|
||||
0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203,
|
||||
]),
|
||||
I = gf([
|
||||
0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7,
|
||||
0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83,
|
||||
]);
|
||||
|
||||
function car25519(o) {
|
||||
var c;
|
||||
var i;
|
||||
for (i = 0; i < 16; i++) {
|
||||
o[i] += 65536;
|
||||
c = Math.floor(o[i] / 65536);
|
||||
o[(i + 1) * (i < 15 ? 1 : 0)] += c - 1 + 37 * (c - 1) * (i === 15 ? 1 : 0);
|
||||
o[i] -= c * 65536;
|
||||
}
|
||||
}
|
||||
|
||||
function sel25519(p, q, b) {
|
||||
var t,
|
||||
c = ~(b - 1);
|
||||
for (var i = 0; i < 16; i++) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
q[i] ^= t;
|
||||
}
|
||||
}
|
||||
|
||||
function unpack25519(o, n) {
|
||||
var i;
|
||||
for (i = 0; i < 16; i++) o[i] = n[2 * i] + (n[2 * i + 1] << 8);
|
||||
o[15] &= 0x7fff;
|
||||
}
|
||||
|
||||
// addition
|
||||
function A(o, a, b) {
|
||||
var i;
|
||||
for (i = 0; i < 16; i++) o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
// subtraction
|
||||
function Z(o, a, b) {
|
||||
var i;
|
||||
for (i = 0; i < 16; i++) o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
// multiplication
|
||||
function M(o, a, b) {
|
||||
var i,
|
||||
j,
|
||||
t = new Float64Array(31);
|
||||
for (i = 0; i < 31; i++) t[i] = 0;
|
||||
for (i = 0; i < 16; i++) {
|
||||
for (j = 0; j < 16; j++) {
|
||||
t[i + j] += a[i] * b[j];
|
||||
}
|
||||
}
|
||||
for (i = 0; i < 15; i++) {
|
||||
t[i] += 38 * t[i + 16];
|
||||
}
|
||||
for (i = 0; i < 16; i++) o[i] = t[i];
|
||||
car25519(o);
|
||||
car25519(o);
|
||||
}
|
||||
|
||||
// squaring
|
||||
function S(o, a) {
|
||||
M(o, a, a);
|
||||
}
|
||||
|
||||
// inversion
|
||||
function inv25519(o, i) {
|
||||
var c = gf();
|
||||
var a;
|
||||
for (a = 0; a < 16; a++) c[a] = i[a];
|
||||
for (a = 253; a >= 0; a--) {
|
||||
S(c, c);
|
||||
if (a !== 2 && a !== 4) M(c, c, i);
|
||||
}
|
||||
for (a = 0; a < 16; a++) o[a] = c[a];
|
||||
}
|
||||
|
||||
function pack25519(o, n) {
|
||||
var i, j, b;
|
||||
var m = gf(),
|
||||
t = gf();
|
||||
for (i = 0; i < 16; i++) t[i] = n[i];
|
||||
car25519(t);
|
||||
car25519(t);
|
||||
car25519(t);
|
||||
for (j = 0; j < 2; j++) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (i = 1; i < 15; i++) {
|
||||
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
|
||||
m[i - 1] &= 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
|
||||
b = (m[15] >> 16) & 1;
|
||||
m[14] &= 0xffff;
|
||||
sel25519(t, m, 1 - b);
|
||||
}
|
||||
for (i = 0; i < 16; i++) {
|
||||
o[2 * i] = t[i] & 0xff;
|
||||
o[2 * i + 1] = t[i] >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
function par25519(a) {
|
||||
var d = new Uint8Array(32);
|
||||
pack25519(d, a);
|
||||
return d[0] & 1;
|
||||
}
|
||||
|
||||
function vn(x, xi, y, yi, n) {
|
||||
var i,
|
||||
d = 0;
|
||||
for (i = 0; i < n; i++) d |= x[xi + i] ^ y[yi + i];
|
||||
return (1 & ((d - 1) >>> 8)) - 1;
|
||||
}
|
||||
|
||||
function crypto_verify_32(x, xi, y, yi) {
|
||||
return vn(x, xi, y, yi, 32);
|
||||
}
|
||||
|
||||
function neq25519(a, b) {
|
||||
var c = new Uint8Array(32),
|
||||
d = new Uint8Array(32);
|
||||
pack25519(c, a);
|
||||
pack25519(d, b);
|
||||
return crypto_verify_32(c, 0, d, 0);
|
||||
}
|
||||
|
||||
function pow2523(o, i) {
|
||||
var c = gf();
|
||||
var a;
|
||||
for (a = 0; a < 16; a++) c[a] = i[a];
|
||||
for (a = 250; a >= 0; a--) {
|
||||
S(c, c);
|
||||
if (a !== 1) M(c, c, i);
|
||||
}
|
||||
for (a = 0; a < 16; a++) o[a] = c[a];
|
||||
}
|
||||
|
||||
function set25519(r, a) {
|
||||
var i;
|
||||
for (i = 0; i < 16; i++) r[i] = a[i] | 0;
|
||||
}
|
||||
|
||||
function unpackneg(r, p) {
|
||||
var t = gf(),
|
||||
chk = gf(),
|
||||
num = gf(),
|
||||
den = gf(),
|
||||
den2 = gf(),
|
||||
den4 = gf(),
|
||||
den6 = gf();
|
||||
|
||||
set25519(r[2], gf1);
|
||||
unpack25519(r[1], p);
|
||||
S(num, r[1]);
|
||||
M(den, num, D);
|
||||
Z(num, num, r[2]);
|
||||
A(den, r[2], den);
|
||||
|
||||
S(den2, den);
|
||||
S(den4, den2);
|
||||
M(den6, den4, den2);
|
||||
M(t, den6, num);
|
||||
M(t, t, den);
|
||||
|
||||
pow2523(t, t);
|
||||
M(t, t, num);
|
||||
M(t, t, den);
|
||||
M(t, t, den);
|
||||
M(r[0], t, den);
|
||||
|
||||
S(chk, r[0]);
|
||||
M(chk, chk, den);
|
||||
if (neq25519(chk, num)) M(r[0], r[0], I);
|
||||
|
||||
S(chk, r[0]);
|
||||
M(chk, chk, den);
|
||||
if (neq25519(chk, num)) return -1;
|
||||
|
||||
if (par25519(r[0]) === p[31] >> 7) Z(r[0], gf0, r[0]);
|
||||
|
||||
M(r[3], r[0], r[1]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ----
|
||||
|
||||
// Converts Ed25519 public key to Curve25519 public key.
|
||||
// montgomeryX = (edwardsY + 1)*inverse(1 - edwardsY) mod p
|
||||
function convertPublicKey(pk) {
|
||||
var z = new Uint8Array(32),
|
||||
q = [gf(), gf(), gf(), gf()],
|
||||
a = gf(),
|
||||
b = gf();
|
||||
|
||||
if (unpackneg(q, pk)) return null; // reject invalid key
|
||||
|
||||
var y = q[1];
|
||||
|
||||
A(a, gf1, y);
|
||||
Z(b, gf1, y);
|
||||
inv25519(b, b);
|
||||
M(a, a, b);
|
||||
|
||||
pack25519(z, a);
|
||||
return z;
|
||||
}
|
||||
|
||||
// Converts Ed25519 secret key to Curve25519 secret key.
|
||||
function convertSecretKey(sk) {
|
||||
var d = new Uint8Array(64),
|
||||
o = new Uint8Array(32),
|
||||
i;
|
||||
nacl.lowlevel.crypto_hash(d, sk, 32);
|
||||
d[0] &= 248;
|
||||
d[31] &= 127;
|
||||
d[31] |= 64;
|
||||
for (i = 0; i < 32; i++) o[i] = d[i];
|
||||
for (i = 0; i < 64; i++) d[i] = 0;
|
||||
return o;
|
||||
}
|
||||
|
||||
function convertKeyPair(edKeyPair) {
|
||||
var publicKey = convertPublicKey(edKeyPair.publicKey);
|
||||
if (!publicKey) return null;
|
||||
return {
|
||||
publicKey: publicKey,
|
||||
secretKey: convertSecretKey(edKeyPair.secretKey),
|
||||
};
|
||||
}
|
||||
|
||||
// return {
|
||||
// convertPublicKey: convertPublicKey,
|
||||
// convertSecretKey: convertSecretKey,
|
||||
// convertKeyPair: convertKeyPair,
|
||||
// };
|
||||
|
||||
module.exports = {
|
||||
convertPublicKey: convertPublicKey,
|
||||
convertSecretKey: convertSecretKey,
|
||||
convertKeyPair: convertKeyPair,
|
||||
};
|
||||
|
||||
// }));
|
||||
2424
server/deps/nacl-fast.js
Normal file
49
server/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
|
||||
const app = express();
|
||||
const http = require("http");
|
||||
const cors = require("cors");
|
||||
const connectDB = require("./db");
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
process.env.TZ = 'America/New_York';
|
||||
|
||||
console.log("Current time in America/New_York:", moment().format());
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
require("dotenv").config();
|
||||
}
|
||||
|
||||
connectDB();
|
||||
|
||||
createUserHome();
|
||||
app.use(cors());
|
||||
app.use(express.json()); // This line is crucial
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || "http://localhost:5173",
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// Serve static files from the React app
|
||||
app.use(express.static(path.join(__dirname, "../client/dist")));
|
||||
|
||||
// The "catchall" handler: for any request that doesn't
|
||||
// match one above, send back React's index.html file.
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../client/dist", "index.html"));
|
||||
});
|
||||
|
||||
|
||||
|
||||
const PORT = process.env.PORT || 3001; // Fallback to 3001 if the PORT env variable is not set
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log("server is running");
|
||||
});
|
||||
BIN
server/memory-pow/memory-pow.wasm
Normal file
BIN
server/memory-pow/memory-pow.wasm.full
Normal file
1
server/memory-pow/memory-pow.wasm.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"names":[],"sources":[],"sourcesContent":[],"mappings":""}
|
||||
34
server/models/Game.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const gameSchema = new mongoose.Schema({
|
||||
players: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], // This remains to keep track of all players in the game
|
||||
playerPayments: [{
|
||||
player: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
payment: { type: mongoose.Schema.Types.ObjectId, ref: 'Payment', default: null } // Reference to their payment
|
||||
}],
|
||||
history: [{
|
||||
state: [[String]], // 2D array representing the game's final state
|
||||
winner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }, // Game winner
|
||||
startedAt: { type: Date, default: Date.now },
|
||||
tie: { type: Boolean, default: false }, // Indicates if the game ended in a tie
|
||||
}],
|
||||
winner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null },
|
||||
status: { type: String, enum: ['waiting', 'active', 'finished', 'finished-forfeit', 'finished-both-disconnected'], default: 'waiting' },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
roomId: { type: String, required: true, unique: true }, // Makes `roomId` a required field
|
||||
payoutStatus: { type: String, enum: ['paid', 'refund', 'pending'], default: 'pending' },
|
||||
payoutPayments: [{
|
||||
player: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
payment: { type: mongoose.Schema.Types.ObjectId, ref: 'Payment', default: null }
|
||||
}],
|
||||
// New fields for series handling
|
||||
series: {
|
||||
totalGames: { type: Number, default: 3 }, // Default to a best-of-3 series
|
||||
scores: [{
|
||||
player: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, // Reference to the User model
|
||||
score: Number
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Game = mongoose.model('Game', gameSchema);
|
||||
12
server/models/Payment.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const paymentSchema = new mongoose.Schema({
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
amount: { type: Number, required: true },
|
||||
game: { type: mongoose.Schema.Types.ObjectId, ref: 'Game', required: true },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
signature: { type: String, required: true }, // New required string field
|
||||
status: { type: String, enum: ['payment-in-not-confirmed', 'payment-in-confirmed', 'payment-out-pending', 'payment-out-done'], default: null }
|
||||
});
|
||||
|
||||
module.exports = Payment = mongoose.model('Payment', paymentSchema);
|
||||
14
server/models/Transaction.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const transactionSchema = new Schema({
|
||||
signature: { type: String, required: true, unique: true },
|
||||
amount: { type: Number, required: true },
|
||||
fromAddress: { type: String, required: true },
|
||||
toAddress: { type: String, required: true },
|
||||
game: { type: mongoose.Schema.Types.ObjectId, ref: 'Game', required: true },
|
||||
});
|
||||
|
||||
const Transaction = mongoose.model('Transaction', transactionSchema);
|
||||
|
||||
module.exports = Transaction;
|
||||
8
server/models/User.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
qortAddress: { type: String, required: true, unique: true }
|
||||
});
|
||||
|
||||
module.exports = User = mongoose.model('User', userSchema);
|
||||
|
||||
1777
server/package-lock.json
generated
Normal file
29
server/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "socket-app-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "nodemon index.js",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"asmcrypto.js": "2.3.2",
|
||||
"axios": "^1.6.8",
|
||||
"bs58": "^5.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"mongoose": "^8.3.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemon": "^3.1.0",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"socket.io": "^4.7.5"
|
||||
}
|
||||
}
|
||||
142
server/transactions/ChatBase.js
Normal file
@@ -0,0 +1,142 @@
|
||||
|
||||
const { QORT_DECIMALS, TX_TYPES } = require("../constants")
|
||||
const { Base58 } = require("../deps/Base58")
|
||||
const { nacl } = require("../deps/nacl-fast")
|
||||
const utils = require('./utils')
|
||||
|
||||
class ChatBase {
|
||||
static get utils() {
|
||||
return utils
|
||||
}
|
||||
static get nacl() {
|
||||
return nacl
|
||||
}
|
||||
static get Base58() {
|
||||
return Base58
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.fee = 0
|
||||
this.groupID = 0
|
||||
this.tests = [
|
||||
() => {
|
||||
if (!(this._type >= 1 && this._type in TX_TYPES)) {
|
||||
return 'Invalid type: ' + this.type
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (this._fee < 0) {
|
||||
return 'Invalid fee: ' + this._fee / QORT_DECIMALS
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (this._groupID < 0 || !Number.isInteger(this._groupID)) {
|
||||
return 'Invalid groupID: ' + this._groupID
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (!(new Date(this._timestamp)).getTime() > 0) {
|
||||
return 'Invalid timestamp: ' + this._timestamp
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (!(this._lastReference instanceof Uint8Array && this._lastReference.byteLength == 64)) {
|
||||
return 'Invalid last reference: ' + this._lastReference
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (!(this._keyPair)) {
|
||||
return 'keyPair must be specified'
|
||||
}
|
||||
if (!(this._keyPair.publicKey instanceof Uint8Array && this._keyPair.publicKey.byteLength === 32)) {
|
||||
return 'Invalid publicKey'
|
||||
}
|
||||
if (!(this._keyPair.privateKey instanceof Uint8Array && this._keyPair.privateKey.byteLength === 64)) {
|
||||
return 'Invalid privateKey'
|
||||
}
|
||||
return true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
set keyPair(keyPair) {
|
||||
this._keyPair = keyPair
|
||||
}
|
||||
|
||||
set type(type) {
|
||||
this.typeText = TX_TYPES[type]
|
||||
this._type = type
|
||||
this._typeBytes = this.constructor.utils.int32ToBytes(this._type)
|
||||
}
|
||||
|
||||
set groupID(groupID) {
|
||||
this._groupID = groupID
|
||||
this._groupIDBytes = this.constructor.utils.int32ToBytes(this._groupID)
|
||||
}
|
||||
|
||||
set timestamp(timestamp) {
|
||||
this._timestamp = timestamp
|
||||
this._timestampBytes = this.constructor.utils.int64ToBytes(this._timestamp)
|
||||
}
|
||||
|
||||
set fee(fee) {
|
||||
this._fee = fee * QORT_DECIMALS
|
||||
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
|
||||
}
|
||||
|
||||
set lastReference(lastReference) {
|
||||
this._lastReference = lastReference instanceof Uint8Array ? lastReference : this.constructor.Base58.decode(lastReference)
|
||||
}
|
||||
|
||||
get params() {
|
||||
return [
|
||||
this._typeBytes,
|
||||
this._timestampBytes,
|
||||
this._groupIDBytes,
|
||||
this._lastReference,
|
||||
this._keyPair.publicKey
|
||||
]
|
||||
}
|
||||
|
||||
get chatBytes() {
|
||||
const isValid = this.validParams()
|
||||
if (!isValid.valid) {
|
||||
throw new Error(isValid.message)
|
||||
}
|
||||
|
||||
let result = new Uint8Array()
|
||||
|
||||
this.params.forEach(item => {
|
||||
result = this.constructor.utils.appendBuffer(result, item)
|
||||
})
|
||||
|
||||
this._chatBytes = result
|
||||
|
||||
return this._chatBytes
|
||||
}
|
||||
|
||||
validParams() {
|
||||
let finalResult = {
|
||||
valid: true
|
||||
}
|
||||
|
||||
this.tests.some(test => {
|
||||
const result = test()
|
||||
if (result !== true) {
|
||||
finalResult = {
|
||||
valid: false,
|
||||
message: result
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
return finalResult
|
||||
}
|
||||
|
||||
}
|
||||
module.exports = { ChatBase };
|
||||
94
server/transactions/ChatTransaction.js
Normal file
@@ -0,0 +1,94 @@
|
||||
|
||||
|
||||
const { ChatBase } = require("./ChatBase.js")
|
||||
const ed2curve = require("../deps/ed2curve.js")
|
||||
const { nacl } = require("../deps/nacl-fast")
|
||||
const {Sha256} = require("asmcrypto.js")
|
||||
const { CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP } = require("../constants/index.js")
|
||||
|
||||
class ChatTransaction extends ChatBase {
|
||||
constructor() {
|
||||
super()
|
||||
this.type = 18
|
||||
this.fee = 0
|
||||
}
|
||||
|
||||
set recipientPublicKey(recipientPublicKey) {
|
||||
this._base58RecipientPublicKey = recipientPublicKey instanceof Uint8Array ? this.constructor.Base58.encode(recipientPublicKey) : recipientPublicKey
|
||||
this._recipientPublicKey = this.constructor.Base58.decode(this._base58RecipientPublicKey)
|
||||
}
|
||||
|
||||
set proofOfWorkNonce(proofOfWorkNonce) {
|
||||
this._proofOfWorkNonce = this.constructor.utils.int32ToBytes(proofOfWorkNonce)
|
||||
}
|
||||
|
||||
set recipient(recipient) {
|
||||
this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient)
|
||||
this._hasReceipient = new Uint8Array(1)
|
||||
this._hasReceipient[0] = 1
|
||||
}
|
||||
|
||||
set hasChatReference(hasChatReference) {
|
||||
this._hasChatReference = new Uint8Array(1)
|
||||
this._hasChatReference[0] = hasChatReference
|
||||
}
|
||||
|
||||
set chatReference(chatReference) {
|
||||
this._chatReference = chatReference instanceof Uint8Array ? chatReference : this.constructor.Base58.decode(chatReference)
|
||||
}
|
||||
|
||||
set message(message) {
|
||||
this.messageText = message;
|
||||
this._message = this.constructor.utils.stringtoUTF8Array(message)
|
||||
this._messageLength = this.constructor.utils.int32ToBytes(this._message.length)
|
||||
}
|
||||
|
||||
set isEncrypted(isEncrypted) {
|
||||
this._isEncrypted = new Uint8Array(1)
|
||||
this._isEncrypted[0] = isEncrypted
|
||||
|
||||
if (isEncrypted === 1) {
|
||||
const convertedPrivateKey = ed2curve.convertSecretKey(this._keyPair.privateKey)
|
||||
const convertedPublicKey = ed2curve.convertPublicKey(this._recipientPublicKey)
|
||||
const sharedSecret = new Uint8Array(32)
|
||||
nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey)
|
||||
|
||||
this._chatEncryptionSeed = new Sha256().process(sharedSecret).finish().result
|
||||
this._encryptedMessage = nacl.secretbox(this._message, this._lastReference.slice(0, 24), this._chatEncryptionSeed)
|
||||
}
|
||||
|
||||
this._myMessage = isEncrypted === 1 ? this._encryptedMessage : this._message
|
||||
this._myMessageLenth = isEncrypted === 1 ? this.constructor.utils.int32ToBytes(this._myMessage.length) : this._messageLength
|
||||
}
|
||||
|
||||
set isText(isText) {
|
||||
this._isText = new Uint8Array(1)
|
||||
this._isText[0] = isText
|
||||
}
|
||||
|
||||
get params() {
|
||||
const params = super.params
|
||||
params.push(
|
||||
this._proofOfWorkNonce,
|
||||
this._hasReceipient,
|
||||
this._recipient,
|
||||
this._myMessageLenth,
|
||||
this._myMessage,
|
||||
this._isEncrypted,
|
||||
this._isText,
|
||||
this._feeBytes
|
||||
)
|
||||
|
||||
// After the feature trigger timestamp we need to include chat reference
|
||||
if (new Date(this._timestamp).getTime() >= CHAT_REFERENCE_FEATURE_TRIGGER_TIMESTAMP) {
|
||||
params.push(this._hasChatReference)
|
||||
|
||||
if (this._hasChatReference[0] == 1) {
|
||||
params.push(this._chatReference)
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ChatTransaction };
|
||||
40
server/transactions/PaymentTransaction.js
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
const { QORT_DECIMALS } = require("../constants")
|
||||
const { TransactionBase } = require("./TransactionBase")
|
||||
|
||||
|
||||
class PaymentTransaction extends TransactionBase {
|
||||
constructor() {
|
||||
super()
|
||||
this.type = 2
|
||||
}
|
||||
|
||||
set recipient(recipient) {
|
||||
this._recipient = recipient instanceof Uint8Array ? recipient : this.constructor.Base58.decode(recipient)
|
||||
}
|
||||
|
||||
set dialogto(dialogto) {
|
||||
this._dialogto = dialogto
|
||||
}
|
||||
|
||||
set dialogamount(dialogamount) {
|
||||
this._dialogamount = dialogamount
|
||||
}
|
||||
|
||||
set amount(amount) {
|
||||
this._amount = Math.round(amount * QORT_DECIMALS)
|
||||
this._amountBytes = this.constructor.utils.int64ToBytes(this._amount)
|
||||
}
|
||||
|
||||
get params() {
|
||||
const params = super.params
|
||||
params.push(
|
||||
this._recipient,
|
||||
this._amountBytes,
|
||||
this._feeBytes
|
||||
)
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PaymentTransaction };
|
||||
165
server/transactions/TransactionBase.js
Normal file
@@ -0,0 +1,165 @@
|
||||
|
||||
const { QORT_DECIMALS, TX_TYPES } = require("../constants")
|
||||
const { Base58 } = require("../deps/Base58")
|
||||
const { nacl } = require("../deps/nacl-fast")
|
||||
const utils = require('./utils')
|
||||
|
||||
class TransactionBase {
|
||||
static get utils() {
|
||||
return utils
|
||||
}
|
||||
static get nacl() {
|
||||
return nacl
|
||||
}
|
||||
static get Base58() {
|
||||
return Base58
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.fee = 0
|
||||
this.groupID = 0
|
||||
this.timestamp = Date.now()
|
||||
this.tests = [
|
||||
() => {
|
||||
if (!(this._type >= 1 && this._type in TX_TYPES)) {
|
||||
return 'Invalid type: ' + this.type
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (this._fee < 0) {
|
||||
return 'Invalid fee: ' + this._fee / QORT_DECIMALS
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (this._groupID < 0 || !Number.isInteger(this._groupID)) {
|
||||
return 'Invalid groupID: ' + this._groupID
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (!(new Date(this._timestamp)).getTime() > 0) {
|
||||
return 'Invalid timestamp: ' + this._timestamp
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (!(this._lastReference instanceof Uint8Array && this._lastReference.byteLength == 64)) {
|
||||
if (this._lastReference == 0) {
|
||||
return 'Invalid last reference. Please ensure that you have at least 0.001 QORT for the transaction fee.'
|
||||
}
|
||||
return 'Invalid last reference: ' + this._lastReference
|
||||
}
|
||||
return true
|
||||
},
|
||||
() => {
|
||||
if (!(this._keyPair)) {
|
||||
return 'keyPair must be specified'
|
||||
}
|
||||
if (!(this._keyPair.publicKey instanceof Uint8Array && this._keyPair.publicKey.byteLength === 32)) {
|
||||
return 'Invalid publicKey'
|
||||
}
|
||||
if (!(this._keyPair.privateKey instanceof Uint8Array && this._keyPair.privateKey.byteLength === 64)) {
|
||||
return 'Invalid privateKey'
|
||||
}
|
||||
return true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
set keyPair(keyPair) {
|
||||
this._keyPair = keyPair
|
||||
}
|
||||
|
||||
set type(type) {
|
||||
this.typeText = TX_TYPES[type]
|
||||
this._type = type
|
||||
this._typeBytes = this.constructor.utils.int32ToBytes(this._type)
|
||||
}
|
||||
|
||||
set groupID(groupID) {
|
||||
this._groupID = groupID
|
||||
this._groupIDBytes = this.constructor.utils.int32ToBytes(this._groupID)
|
||||
}
|
||||
|
||||
set timestamp(timestamp) {
|
||||
this._timestamp = timestamp
|
||||
this._timestampBytes = this.constructor.utils.int64ToBytes(this._timestamp)
|
||||
}
|
||||
|
||||
set fee(fee) {
|
||||
this._fee = fee * QORT_DECIMALS
|
||||
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
|
||||
}
|
||||
|
||||
set lastReference(lastReference) {
|
||||
this._lastReference = lastReference instanceof Uint8Array ? lastReference : this.constructor.Base58.decode(lastReference)
|
||||
}
|
||||
|
||||
get params() {
|
||||
return [
|
||||
this._typeBytes,
|
||||
this._timestampBytes,
|
||||
this._groupIDBytes,
|
||||
this._lastReference,
|
||||
this._keyPair.publicKey
|
||||
]
|
||||
}
|
||||
|
||||
get signedBytes() {
|
||||
if (!this._signedBytes) {
|
||||
this.sign()
|
||||
}
|
||||
return this._signedBytes
|
||||
}
|
||||
|
||||
validParams() {
|
||||
let finalResult = {
|
||||
valid: true
|
||||
}
|
||||
this.tests.some(test => {
|
||||
const result = test()
|
||||
if (result !== true) {
|
||||
finalResult = {
|
||||
valid: false,
|
||||
message: result
|
||||
}
|
||||
return true // exists the loop
|
||||
}
|
||||
})
|
||||
return finalResult
|
||||
}
|
||||
|
||||
generateBase() {
|
||||
const isValid = this.validParams()
|
||||
if (!isValid.valid) {
|
||||
throw new Error(isValid.message)
|
||||
}
|
||||
let result = new Uint8Array()
|
||||
|
||||
this.params.forEach(item => {
|
||||
result = this.constructor.utils.appendBuffer(result, item)
|
||||
})
|
||||
|
||||
this._base = result
|
||||
return result
|
||||
}
|
||||
|
||||
sign() {
|
||||
if (!this._keyPair) {
|
||||
throw new Error('keyPair not defined')
|
||||
}
|
||||
|
||||
if (!this._base) {
|
||||
this.generateBase()
|
||||
}
|
||||
|
||||
this._signature = this.constructor.nacl.sign.detached(this._base, this._keyPair.privateKey)
|
||||
|
||||
this._signedBytes = this.constructor.utils.appendBuffer(this._base, this._signature)
|
||||
|
||||
return this._signature
|
||||
}
|
||||
}
|
||||
module.exports = { TransactionBase };
|
||||
263
server/transactions/transactions.js
Normal file
@@ -0,0 +1,263 @@
|
||||
const bs58 = require("bs58");
|
||||
|
||||
const {Base58} = require("../deps/Base58");
|
||||
const axios = require("axios");
|
||||
const { findUsableApi } = require("../utils");
|
||||
const { PaymentTransaction } = require("./PaymentTransaction");
|
||||
const { ChatTransaction } = require("./ChatTransaction");
|
||||
|
||||
const { homeAddress } = require("../constants");
|
||||
const { nacl } = require("../deps/nacl-fast");
|
||||
const utils = require("./utils");
|
||||
|
||||
const transactionTypes = {
|
||||
2: PaymentTransaction,
|
||||
18: ChatTransaction
|
||||
};
|
||||
|
||||
const base58ToUint8Array = (base58Encoded) => {
|
||||
const bytes = bs58.decode(base58Encoded);
|
||||
|
||||
// Convert the Buffer to Uint8Array
|
||||
const uint8Array = new Uint8Array(bytes);
|
||||
return uint8Array;
|
||||
};
|
||||
|
||||
const createKeyPair = () => {
|
||||
const publicKey = base58ToUint8Array(process.env.KEY_PAIR_PUBLIC);
|
||||
const privateKey = base58ToUint8Array(process.env.KEY_PAIR_PRIVATE);
|
||||
return {
|
||||
publicKey,
|
||||
privateKey,
|
||||
};
|
||||
};
|
||||
|
||||
const createTransaction = (type, keyPair, params) => {
|
||||
const tx = new transactionTypes[type]();
|
||||
tx.keyPair = keyPair;
|
||||
Object.keys(params).forEach((param) => {
|
||||
tx[param] = params[param];
|
||||
});
|
||||
|
||||
return tx;
|
||||
};
|
||||
|
||||
const processTransactionVersion2 = async (body, validApi) => {
|
||||
const url = validApi + "/transactions/process?apiVersion=2";
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, body);
|
||||
return response.data; // Directly return the parsed JSON data
|
||||
} catch (error) {
|
||||
// Axios error handling
|
||||
if (error.response) {
|
||||
|
||||
// The server responded with a status code outside of the 2xx range
|
||||
console.error('Error response:', error.response.data);
|
||||
throw error // Returning the server's error message
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error('No response received:', error.request);
|
||||
} else {
|
||||
// Something else happened in setting up the request
|
||||
console.error('Error setting up the request:', error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const transaction = async ({ type, params, apiVersion, keyPair }, validApi) => {
|
||||
const tx = createTransaction(type, keyPair, params);
|
||||
let res;
|
||||
|
||||
if (apiVersion && apiVersion === 2) {
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
res = await processTransactionVersion2(signedBytes, validApi);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: res,
|
||||
};
|
||||
};
|
||||
|
||||
const makeTransactionRequest = async (
|
||||
receiver,
|
||||
lastRef,
|
||||
amount,
|
||||
fee,
|
||||
keyPair,
|
||||
validApi
|
||||
) => {
|
||||
try {
|
||||
|
||||
const myTxnrequest = await transaction(
|
||||
{
|
||||
nonce: 0,
|
||||
type: 2,
|
||||
params: {
|
||||
recipient: receiver,
|
||||
amount: amount,
|
||||
lastReference: lastRef,
|
||||
fee: fee,
|
||||
},
|
||||
apiVersion: 2,
|
||||
keyPair,
|
||||
},
|
||||
validApi
|
||||
);
|
||||
return myTxnrequest;
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const validateAddress = (address) => {
|
||||
let isAddress = false;
|
||||
try {
|
||||
const decodePubKey = Base58.decode(address);
|
||||
|
||||
if (!(decodePubKey instanceof Uint8Array && decodePubKey.length == 25)) {
|
||||
isAddress = false;
|
||||
} else {
|
||||
isAddress = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return isAddress;
|
||||
};
|
||||
|
||||
const getNameOrAddress = async (receiver) => {
|
||||
try {
|
||||
const isAddress = validateAddress(receiver);
|
||||
if (isAddress) {
|
||||
return receiver;
|
||||
}
|
||||
const validApi = await findUsableApi();
|
||||
|
||||
// Using axios to replace fetch
|
||||
const response = await axios.get(`${validApi}/names/${receiver}`);
|
||||
const data = response.data;
|
||||
if (data?.owner) return data.owner;
|
||||
if (data?.error) {
|
||||
throw new Error("Name does not exist");
|
||||
}
|
||||
// Axios handles HTTP error status automatically in the catch block
|
||||
return { error: "cannot validate address or name" };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"cannot validate address or name"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getLastRef = async (address) => {
|
||||
const validApi = await findUsableApi();
|
||||
|
||||
// Using axios to replace fetch
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${validApi}/addresses/lastreference/${address}`
|
||||
);
|
||||
return response.data; // Axios handles text content via data attribute
|
||||
} catch (error) {
|
||||
throw new Error("Cannot fetch balance");
|
||||
}
|
||||
};
|
||||
|
||||
const sendQortFee = async () => {
|
||||
const validApi = await findUsableApi();
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${validApi}/transactions/unitfee?txType=PAYMENT`
|
||||
);
|
||||
const data = response.data;
|
||||
const qortFee = (Number(data) / 1e8).toFixed(8);
|
||||
return qortFee;
|
||||
} catch (error) {
|
||||
throw new Error("Error when fetching join fee");
|
||||
}
|
||||
};
|
||||
|
||||
const sendCoin = async ({ amount, receiver }) => {
|
||||
try {
|
||||
const confirmReceiver = await getNameOrAddress(receiver);
|
||||
if (confirmReceiver.error)
|
||||
throw new Error("Invalid receiver address or name");
|
||||
const keyPair = createKeyPair();
|
||||
const lastRef = await getLastRef(homeAddress);
|
||||
const fee = await sendQortFee();
|
||||
const validApi = await findUsableApi();
|
||||
|
||||
const res = await makeTransactionRequest(
|
||||
confirmReceiver,
|
||||
lastRef,
|
||||
amount,
|
||||
fee,
|
||||
keyPair,
|
||||
validApi
|
||||
);
|
||||
return { res, validApi };
|
||||
} catch (error) {
|
||||
console.error(error?.message)
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const checkBlockchain = async (signature)=> {
|
||||
try {
|
||||
const validApi = await findUsableApi();
|
||||
const response = await axios.get(
|
||||
`${validApi}/transactions/signature/${signature}`
|
||||
);
|
||||
return response.data
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const signChat = (chatBytes, nonce, privateKey) => {
|
||||
|
||||
if (!chatBytes) {
|
||||
throw new Error('Chat Bytes not defined')
|
||||
}
|
||||
|
||||
if (!nonce) {
|
||||
throw new Error('Nonce not defined')
|
||||
}
|
||||
|
||||
if (!privateKey) {
|
||||
throw new Error('keyPair not defined')
|
||||
}
|
||||
|
||||
const _nonce = utils.int32ToBytes(nonce)
|
||||
if (chatBytes.length === undefined) {
|
||||
const _chatBytesBuffer = Object.keys(chatBytes).map(function (key) { return chatBytes[key]; })
|
||||
|
||||
const chatBytesBuffer = new Uint8Array(_chatBytesBuffer)
|
||||
chatBytesBuffer.set(_nonce, 112)
|
||||
|
||||
const signature = nacl.sign.detached(chatBytesBuffer, privateKey)
|
||||
|
||||
return utils.appendBuffer(chatBytesBuffer, signature)
|
||||
} else {
|
||||
const chatBytesBuffer = new Uint8Array(chatBytes)
|
||||
chatBytesBuffer.set(_nonce, 112)
|
||||
|
||||
const signature = nacl.sign.detached(chatBytesBuffer, privateKey)
|
||||
|
||||
return utils.appendBuffer(chatBytesBuffer, signature)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = { sendCoin, checkBlockchain, transaction, signChat, base58ToUint8Array, createKeyPair, createTransaction };
|
||||
54
server/transactions/utils.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const utils = {
|
||||
int32ToBytes (word) {
|
||||
var byteArray = []
|
||||
for (var b = 0; b < 32; b += 8) {
|
||||
byteArray.push((word >>> (24 - b % 32)) & 0xFF)
|
||||
}
|
||||
return byteArray
|
||||
},
|
||||
|
||||
stringtoUTF8Array (message) {
|
||||
if (typeof message === 'string') {
|
||||
var s = unescape(encodeURIComponent(message)) // UTF-8
|
||||
message = new Uint8Array(s.length)
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
message[i] = s.charCodeAt(i) & 0xff
|
||||
}
|
||||
}
|
||||
return message
|
||||
},
|
||||
// ...buffers then buffers.foreach and append to buffer1
|
||||
appendBuffer (buffer1, buffer2) {
|
||||
buffer1 = new Uint8Array(buffer1)
|
||||
buffer2 = new Uint8Array(buffer2)
|
||||
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength)
|
||||
tmp.set(buffer1, 0)
|
||||
tmp.set(buffer2, buffer1.byteLength)
|
||||
return tmp
|
||||
},
|
||||
|
||||
int64ToBytes (int64) {
|
||||
// we want to represent the input as a 8-bytes array
|
||||
var byteArray = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
for (var index = 0; index < byteArray.length; index++) {
|
||||
var byte = int64 & 0xff
|
||||
byteArray[byteArray.length - index - 1] = byte
|
||||
int64 = (int64 - byte) / 256
|
||||
}
|
||||
|
||||
return byteArray
|
||||
},
|
||||
|
||||
equal (buf1, buf2) {
|
||||
if (buf1.byteLength != buf2.byteLength) return false
|
||||
var dv1 = new Uint8Array(buf1)
|
||||
var dv2 = new Uint8Array(buf2)
|
||||
for (var i = 0; i != buf1.byteLength; i++) {
|
||||
if (dv1[i] != dv2[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = utils;
|
||||
38
server/utils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const apiEndpoints = [
|
||||
"https://appnode.qortal.org",
|
||||
"https://api.qortal.org",
|
||||
"https://api2.qortal.org",
|
||||
"https://apinode.qortalnodes.live",
|
||||
"https://apinode1.qortalnodes.live",
|
||||
"https://apinode2.qortalnodes.live",
|
||||
"https://apinode3.qortalnodes.live",
|
||||
"https://apinode4.qortalnodes.live",
|
||||
];
|
||||
|
||||
const findUsableApi = async() => {
|
||||
for (const endpoint of apiEndpoints) {
|
||||
try {
|
||||
// Set timeout to 3000 milliseconds (3 seconds)
|
||||
const response = await axios.get(`${endpoint}/admin/status`, { timeout: 3000 });
|
||||
const data = response.data;
|
||||
if (data.isSynchronizing === false && data.syncPercent === 100) {
|
||||
console.log(`Usable API found: ${endpoint}`);
|
||||
return endpoint;
|
||||
} else {
|
||||
console.log(`API not ready: ${endpoint}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
console.log(`Timeout reached for API ${endpoint}`);
|
||||
} else {
|
||||
console.error(`Error checking API ${endpoint}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No usable API found");
|
||||
}
|
||||
|
||||
module.exports = { findUsableApi };
|
||||