rematch proto
This commit is contained in:
parent
c37f8f7457
commit
de435d89aa
115
package-lock.json
generated
115
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@fontsource/manrope": "^5.2.5",
|
||||
"@privy-io/react-auth": "^2.7.2",
|
||||
"@solana/web3.js": "^1.98.0",
|
||||
"axios": "^1.8.4",
|
||||
"bs58": "^6.0.0",
|
||||
"ethers": "^6.13.5",
|
||||
"next": "15.2.4",
|
||||
|
|
@ -4177,6 +4178,12 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
|
|
@ -4212,6 +4219,17 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
|
|
@ -4438,7 +4456,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
|
|
@ -4641,6 +4658,18 @@
|
|||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
|
|
@ -4906,6 +4935,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
|
||||
|
|
@ -4963,7 +5001,6 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
|
|
@ -5122,7 +5159,6 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -5132,7 +5168,6 @@
|
|||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -5170,7 +5205,6 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
|
|
@ -5183,7 +5217,6 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
|
|
@ -6045,6 +6078,26 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
|
|
@ -6061,11 +6114,25 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
|
@ -6115,7 +6182,6 @@
|
|||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
|
|
@ -6140,7 +6206,6 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
|
|
@ -6228,7 +6293,6 @@
|
|||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -6324,7 +6388,6 @@
|
|||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -6337,7 +6400,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
|
|
@ -6363,7 +6425,6 @@
|
|||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
|
|
@ -7569,7 +7630,6 @@
|
|||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -7616,6 +7676,27 @@
|
|||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
|
|
@ -8430,6 +8511,12 @@
|
|||
"integrity": "sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"@fontsource/manrope": "^5.2.5",
|
||||
"@privy-io/react-auth": "^2.7.2",
|
||||
"@solana/web3.js": "^1.98.0",
|
||||
"axios": "^1.8.4",
|
||||
"bs58": "^6.0.0",
|
||||
"ethers": "^6.13.5",
|
||||
"next": "15.2.4",
|
||||
|
|
|
|||
8
public/UnityBuild/bridge_template
Normal file
8
public/UnityBuild/bridge_template
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*UNITY BRIDGE*/
|
||||
function sendMessageToReact(message) {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(message, "*"); // Replace "*" with your React app's origin for security
|
||||
} else {
|
||||
console.error("Parent window not found.");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -1,13 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Unity WebGL Player | TetrisMP</title>
|
||||
<link rel="shortcut icon" href="TemplateData/favicon.ico">
|
||||
<link rel="stylesheet" href="TemplateData/style.css">
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="unity-container" class="unity-desktop">
|
||||
<canvas id="unity-canvas" width=1280 height=720 tabindex="-1"></canvas>
|
||||
<div id="unity-loading-bar">
|
||||
|
|
@ -48,7 +50,7 @@
|
|||
if (type == 'error') div.style = 'background: red; padding: 10px;';
|
||||
else {
|
||||
if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
warningBanner.removeChild(div);
|
||||
updateBannerVisibility();
|
||||
}, 5000);
|
||||
|
|
@ -69,6 +71,14 @@
|
|||
showBanner: unityShowBanner,
|
||||
};
|
||||
|
||||
/*UNITY BRIDGE*/
|
||||
function sendMessageToReact(message) {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(message, "*"); // Replace "*" with your React app's origin for security
|
||||
} else {
|
||||
console.error("Parent window not found.");
|
||||
}
|
||||
}
|
||||
// By default, Unity keeps WebGL canvas render target size matched with
|
||||
// the DOM size of the canvas element (scaled by window.devicePixelRatio)
|
||||
// Set this to false if you want to decouple this synchronization from
|
||||
|
|
@ -118,5 +128,6 @@
|
|||
document.body.appendChild(script);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -94,3 +94,63 @@ body {
|
|||
.modal-exit {
|
||||
animation: slide-up 0.2s ease-in;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Hovering card */
|
||||
/* Ensure the card container is positioned relatively for absolute positioning */
|
||||
.card-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background-color: #2D2D2D;
|
||||
border: 1px solid #4B4B4B;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* This will ensure that the content (text, images) stays in place */
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Style the profile image */
|
||||
.card-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Style for the thumbnail image */
|
||||
.card-thumbnail {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Positioning and styling of the 'View TX' button */
|
||||
.view-tx-btn {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
background-color: #007BFF;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
border-radius: 8px 0 0 8px; /* Rounded left side */
|
||||
}
|
||||
|
||||
/* On hover, slide the card to the left and show the button */
|
||||
.card-container:hover .view-tx-btn {
|
||||
opacity: 1;
|
||||
transform: translateX(0); /* Slide the button to the left */
|
||||
}
|
||||
|
|
|
|||
205
src/components/GameHistory.tsx
Normal file
205
src/components/GameHistory.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { MouseEventHandler, useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Game } from "@/types/Game";
|
||||
import { games } from "@/data/games";
|
||||
import { WAGER_PRIZE_MULT } from "@/shared/constants";
|
||||
import { EXPLORER_ADDRESS_TEMPLATE } from "@/data/shared";
|
||||
|
||||
interface GameHistory {
|
||||
address: string;
|
||||
master_score: string;
|
||||
client_score: string;
|
||||
winner: string;
|
||||
wager: string;
|
||||
master_id: string;
|
||||
client_id: string;
|
||||
game: string; // game ID
|
||||
}
|
||||
|
||||
interface Opponent {
|
||||
username: string;
|
||||
x_profile_url: string;
|
||||
}
|
||||
|
||||
interface GameHistoryModalProps {
|
||||
userId: string | undefined;
|
||||
isOpen: boolean;
|
||||
onClose: MouseEventHandler;
|
||||
}
|
||||
|
||||
export default function GameHistoryModal({
|
||||
userId,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: GameHistoryModalProps) {
|
||||
const [gamesHistory, setGamesHistory] = useState<GameHistory[]>([]);
|
||||
const [opponentInfo, setOpponentInfo] = useState<{
|
||||
[key: string]: Opponent;
|
||||
}>({});
|
||||
const [gameImages, setGameImages] = useState<{ [key: string]: string }>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !userId) return;
|
||||
|
||||
const fetchGames = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(
|
||||
`https://vps.playpoolstudios.com/duelfi/api/get_game_history.php?uid=${userId}`
|
||||
);
|
||||
const gameData = res.data || [];
|
||||
setGamesHistory(gameData);
|
||||
|
||||
const opponentIds: string[] = gameData.map((game: GameHistory) =>
|
||||
game.master_id === userId ? game.client_id : game.master_id
|
||||
);
|
||||
const uniqueOpponentIds: string[] = Array.from(new Set(opponentIds));
|
||||
|
||||
const fetchedOpponentInfo: { [key: string]: Opponent } = {};
|
||||
|
||||
await Promise.all(
|
||||
uniqueOpponentIds.map(async (uid) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://vps.playpoolstudios.com/duelfi/api/get_user_by_id.php?id=${uid}`
|
||||
);
|
||||
const { username, x_profile_url } = response.data;
|
||||
fetchedOpponentInfo[uid] = {
|
||||
username,
|
||||
x_profile_url,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch opponent info for", uid, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setOpponentInfo(fetchedOpponentInfo);
|
||||
|
||||
const gameDataWithImages: { [key: string]: string } = {};
|
||||
await Promise.all(
|
||||
gameData.map(async (gameHistory: GameHistory) => {
|
||||
try {
|
||||
const gameImage = games.find((game: Game) => game.id == gameHistory.game);
|
||||
gameDataWithImages[gameHistory.game] = gameImage?.thumbnail ?? "";
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch game image for", gameHistory.game, err);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setGameImages(gameDataWithImages);
|
||||
} catch (err) {
|
||||
console.error("Error fetching game history", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchGames();
|
||||
}, [isOpen, userId]);
|
||||
|
||||
const handleViewTxClick = (address: string) => {
|
||||
// Open the transaction in a new tab (you can modify this URL to dynamically handle the TX)
|
||||
window.open(`${EXPLORER_ADDRESS_TEMPLATE.replace("{address}",address)}`, "_blank");
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 right-0 h-full w-[400px] bg-[rgb(10,10,10)] shadow-lg z-50 p-6 overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold text-white">Game History</h2>
|
||||
<button onClick={onClose} className="text-red-500 font-bold text-lg">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
) : gamesHistory.length === 0 ? (
|
||||
<p className="text-gray-500">No games played yet.</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{gamesHistory.map((game, idx) => {
|
||||
const isUserMaster = game.master_id === userId;
|
||||
const userScore = isUserMaster
|
||||
? game.master_score
|
||||
: game.client_score;
|
||||
const opponentScore = isUserMaster
|
||||
? game.client_score
|
||||
: game.master_score;
|
||||
const opponentId = isUserMaster
|
||||
? game.client_id
|
||||
: game.master_id;
|
||||
const didUserWin =
|
||||
(isUserMaster && game.winner === "master") ||
|
||||
(!isUserMaster && game.winner === "client");
|
||||
|
||||
const opponent = opponentInfo[opponentId];
|
||||
const profileUrl =
|
||||
opponent?.x_profile_url ||
|
||||
(opponent?.username
|
||||
? `https://vps.playpoolstudios.com/duelfi/profile_picture/${opponent.username}.jpg`
|
||||
: "/duelfiassets/default-avatar.png");
|
||||
|
||||
const gameImageUrl = gameImages[game.game] || "/duelfiassets/default-game-thumbnail.png";
|
||||
|
||||
const wagerAmount = parseFloat(game.wager);
|
||||
const outcomeText = didUserWin
|
||||
? `+${(wagerAmount / 1e8) * 2 * WAGER_PRIZE_MULT} SOL`
|
||||
: `-${(wagerAmount / 1e8)} SOL`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative border border-[rgb(30,30,30)] rounded-xl p-3 flex gap-3 items-center group"
|
||||
>
|
||||
{/* Card content */}
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-white">
|
||||
{opponent?.username || "Unknown Opponent"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
Score: {userScore} - {opponentScore}
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm font-semibold ${
|
||||
didUserWin ? "text-green-500" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{didUserWin ? "You won" : "You lost"}
|
||||
</p>
|
||||
<p className={`text-xs ${didUserWin ? "text-green-500" : "text-red-500"}`}>
|
||||
{outcomeText}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
src={profileUrl}
|
||||
alt="Profile"
|
||||
className="w-10 h-10 rounded-full border border-gray-700 object-cover"
|
||||
/>
|
||||
<img
|
||||
src={gameImageUrl}
|
||||
alt="Game Thumbnail"
|
||||
className="w-16 h-16 rounded-md object-cover ml-4"
|
||||
/>
|
||||
|
||||
{/* View TX Action */}
|
||||
<div className="absolute top-0 right-0 h-full w-28 bg-blue-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 group-hover:translate-x-0 transition-all">
|
||||
<button
|
||||
onClick={() => handleViewTxClick(game.address)}
|
||||
className="px-4 py-2 text-sm font-semibold"
|
||||
>
|
||||
View TX
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) {
|
|||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const tx = await createBet(wallet, user?.id ?? "", selectedPrice, selectedGame);
|
||||
const tx = await createBet(wallet, user?.id ?? "", selectedPrice, selectedGame.id, false);
|
||||
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
|
||||
if (tx.length > 5) {
|
||||
toast.success(`Bet created successfully!`, {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import OpenGames from "./OpenGames";
|
||||
import GameModal from "./GameModal";
|
||||
import { HowItWorksModal } from "./HowItWorksModal";
|
||||
import YourGames from "./YourGames";
|
||||
import { Bet } from "@/types/Bet";
|
||||
import { fetchOpenBets } from "@/shared/solana_helpers";
|
||||
import { fetchOpenBets, createBet, joinBet, getVaultByAddress } from "@/shared/solana_helpers";
|
||||
import { ConnectedSolanaWallet, usePrivy, useSolanaWallets } from "@privy-io/react-auth";
|
||||
import { RematchModal } from "./RematchModal";
|
||||
|
||||
export default function HeroSection() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
|
@ -16,60 +17,284 @@ export default function HeroSection() {
|
|||
const [bets, setBets] = useState<Bet[]>([]);
|
||||
const [solWallet, setSolWallet] = useState<ConnectedSolanaWallet>();
|
||||
const [myActiveBet, setMyActiveBet] = useState<Bet>();
|
||||
const { wallets, ready } = useSolanaWallets();
|
||||
const {user} = usePrivy();
|
||||
const [rematch, setRematch] = useState(false);
|
||||
const [lastActiveBet, setLastActiveBet] = useState<Bet>();
|
||||
const [rematchInProgress, setRematchInProgress] = useState(false);
|
||||
const [rematchTxError, setRematchTxError] = useState(false);
|
||||
const [rematchBetAddress, setRematchBetAddress] = useState<string | null>(null);
|
||||
|
||||
async function updateBets() {
|
||||
const { wallets, ready } = useSolanaWallets();
|
||||
const { user } = usePrivy();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if(rematch){
|
||||
if(!lastActiveBet){
|
||||
console.log(`last active bet was null, heres active bet ${myActiveBet}`);
|
||||
}
|
||||
const isOwner = lastActiveBet?.owner_id == user?.id;
|
||||
console.log(`rematch function ${isOwner ? "owner" : "joiner"} owner_id:${lastActiveBet?.owner_id}`);
|
||||
|
||||
if(isOwner){
|
||||
handleCreateRematch();
|
||||
}else{
|
||||
handleJoinRematch();
|
||||
}
|
||||
}
|
||||
|
||||
},[rematch])
|
||||
|
||||
const game_close_signal = (status: number) => {
|
||||
setRematch(status == 1);
|
||||
setMyActiveBet(undefined);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const updateBets = async () => {
|
||||
if (!ready || wallets.length === 0) return;
|
||||
|
||||
// Find the wallet to use (either Solana wallet or the first available wallet)
|
||||
const wallet = wallets.find((_wallet) => _wallet.type === "solana") || wallets[0];
|
||||
setSolWallet(wallet);
|
||||
// Fetch the open bets
|
||||
|
||||
const fetchedBets = await fetchOpenBets(wallet);
|
||||
|
||||
|
||||
|
||||
// Filter out the bets that are not filled (do not have both owner_id and joiner_id)
|
||||
const filteredBets = fetchedBets.filter((bet) => !(bet.owner_id && bet.joiner_id));
|
||||
|
||||
// Create a new list for filled bets (bets that have both owner_id and joiner_id set)
|
||||
const filledBets = fetchedBets.filter((bet) => bet.owner_id && bet.joiner_id);
|
||||
const activeBet = filledBets.find((bet)=> bet.owner_id == user?.id || bet.joiner_id==user?.id);
|
||||
console.log("My active bet:", activeBet);
|
||||
console.log("Filtered bets:", filteredBets);
|
||||
console.log("Filled bets:", filledBets); // This is the new list for filled bets
|
||||
const activeBet = filledBets.find((bet) => bet.owner_id === user?.id || bet.joiner_id === user?.id);
|
||||
|
||||
// Set the state for filtered bets and active bet
|
||||
setBets(filteredBets);
|
||||
setMyActiveBet(activeBet);
|
||||
|
||||
// If needed, you can also use the filledBets list for something else
|
||||
if(rematch){
|
||||
setMyActiveBet(undefined);
|
||||
return;
|
||||
}
|
||||
setBets(filteredBets);
|
||||
if (!myActiveBet && lastActiveBet?.address !== activeBet?.address) {
|
||||
setMyActiveBet(activeBet);
|
||||
setLastActiveBet(activeBet);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateRematch = async () => {
|
||||
console.log('Creating rematch...');
|
||||
if (!lastActiveBet || !solWallet) return;
|
||||
|
||||
setRematchInProgress(true);
|
||||
setRematchTxError(false);
|
||||
|
||||
try {
|
||||
// Step 1: Create new bet
|
||||
const tx = await createBet(
|
||||
solWallet,
|
||||
user?.id ?? "",
|
||||
lastActiveBet.wager,
|
||||
lastActiveBet.id,
|
||||
true
|
||||
);
|
||||
console.log("Rematch created. Transaction ID:", tx);
|
||||
|
||||
// Step 2: Inform backend of rematch link
|
||||
const set_response = await fetch(
|
||||
`https://vps.playpoolstudios.com/duelfi/api/set_rematch_address.php?address=${lastActiveBet.address}&rematch_address=${tx}`
|
||||
);
|
||||
console.log(await set_response.text());
|
||||
|
||||
// Step 3: Poll until vault account is found
|
||||
const pollUntilFound = async (
|
||||
maxRetries = 10,
|
||||
delayMs = 3000
|
||||
): Promise<any> => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
const vault = await getVaultByAddress(solWallet, tx);
|
||||
if (vault) return vault;
|
||||
console.log(`Waiting for vault account... (${i + 1}/${maxRetries})`);
|
||||
await new Promise(res => setTimeout(res, delayMs));
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const newBetAcc = await pollUntilFound();
|
||||
|
||||
if (!newBetAcc) {
|
||||
console.error("Failed to retrieve new bet vault after retries.");
|
||||
setRematchTxError(true);
|
||||
} else {
|
||||
// Step 4: Set new active bet
|
||||
setMyActiveBet(newBetAcc);
|
||||
setLastActiveBet(newBetAcc);
|
||||
setRematch(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Create rematch failed:", err);
|
||||
setRematchTxError(true);
|
||||
} finally {
|
||||
setRematchInProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJoinRematch = async () => {
|
||||
if (!lastActiveBet || !solWallet) return;
|
||||
|
||||
try {
|
||||
const pollForRematchAddress = async (
|
||||
maxRetries = 10,
|
||||
delayMs = 3000
|
||||
): Promise<string | null> => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
console.log(`Polling rematch address... (${i + 1}/${maxRetries})`);
|
||||
const response = await fetch(
|
||||
`https://vps.playpoolstudios.com/duelfi/api/get_rematch_address.php?address=${lastActiveBet.address}`
|
||||
);
|
||||
const rematchAddress = (await response.text()).trim();
|
||||
|
||||
if (rematchAddress.length > 5) {
|
||||
console.log("Found rematch address:", rematchAddress);
|
||||
return rematchAddress;
|
||||
}
|
||||
|
||||
await new Promise(res => setTimeout(res, delayMs));
|
||||
}
|
||||
|
||||
console.warn("Rematch address not found after max retries.");
|
||||
return null;
|
||||
};
|
||||
|
||||
const rematchAddress = await pollForRematchAddress();
|
||||
|
||||
if (!rematchAddress) return;
|
||||
|
||||
const pollForVault = async (
|
||||
address: string,
|
||||
maxRetries = 10,
|
||||
delayMs = 3000
|
||||
): Promise<any> => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
console.log(`Polling vault for address ${address}... (${i + 1}/${maxRetries})`);
|
||||
const vault = await getVaultByAddress(solWallet, address);
|
||||
if (vault) return vault;
|
||||
await new Promise(res => setTimeout(res, delayMs));
|
||||
}
|
||||
|
||||
console.warn("Vault not found after max retries.");
|
||||
return null;
|
||||
};
|
||||
|
||||
const rematchVault:Bet = await pollForVault(rematchAddress);
|
||||
|
||||
if (rematchVault) {
|
||||
const tx = await joinBet(solWallet, user?.id!, rematchVault.id, rematchVault.address);
|
||||
setMyActiveBet(rematchVault);
|
||||
setLastActiveBet(rematchVault);
|
||||
setRematch(false);
|
||||
console.log("Rematch vault set as active.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error during handleJoinRematch:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleJoinRematch = async () => {
|
||||
// console.log('rejoining rematch');
|
||||
|
||||
// if (!solWallet || !lastActiveBet?.owner_id || !lastActiveBet?.wager) return;
|
||||
|
||||
// setRematchInProgress(true);
|
||||
// setRematchTxError(false);
|
||||
|
||||
// const maxAttempts = 50;
|
||||
// let attempts = 0;
|
||||
|
||||
// const pollForMatchingBet = async (): Promise<Bet | null> => {
|
||||
// try {
|
||||
// const openBets = await fetchOpenBets(solWallet);
|
||||
// const match = openBets.find(
|
||||
// (bet) =>
|
||||
// bet.owner_id === lastActiveBet.owner_id &&
|
||||
// bet.wager === lastActiveBet.wager &&
|
||||
// bet.address !== lastActiveBet.address // to ensure it's a new one
|
||||
// );
|
||||
// return match ?? null;
|
||||
// } catch (err) {
|
||||
// console.error("Error fetching bets:", err);
|
||||
// return null;
|
||||
// }
|
||||
// };
|
||||
|
||||
// const tryJoin = async () => {
|
||||
// while (attempts < maxAttempts) {
|
||||
// const matchingBet = await pollForMatchingBet();
|
||||
// if (matchingBet) {
|
||||
// try {
|
||||
// setRematchBetAddress(matchingBet.address);
|
||||
// const tx = await joinBet(
|
||||
// solWallet,
|
||||
// user?.id ?? "",
|
||||
// lastActiveBet.id ?? "tetris",
|
||||
// matchingBet.address
|
||||
// );
|
||||
// console.log("Joined rematch:", tx);
|
||||
// setRematch(false);
|
||||
// return;
|
||||
// } catch (err) {
|
||||
// console.error("Join rematch failed:", err);
|
||||
// setRematchTxError(true);
|
||||
// return;
|
||||
// } finally {
|
||||
// setRematchInProgress(false);
|
||||
// }
|
||||
// }
|
||||
|
||||
// attempts++;
|
||||
// await new Promise((res) => setTimeout(res, 5000));
|
||||
// }
|
||||
|
||||
// // Max attempts reached
|
||||
// setRematchTxError(true);
|
||||
// setRematchInProgress(false);
|
||||
// };
|
||||
|
||||
// tryJoin();
|
||||
// };
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) return;
|
||||
|
||||
updateBets();
|
||||
const interval = setInterval(updateBets, 10000);
|
||||
const interval = setInterval(updateBets, 3500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [ready]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.origin === window.location.origin && typeof event.data === "string") {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
if (message?.type === "gameClose" && typeof message.status === "number") {
|
||||
game_close_signal(message.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("JSON parse error from Unity message:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{myActiveBet ? (
|
||||
// Render Unity WebGL game when myActiveBet is found
|
||||
{myActiveBet && !rematch ? (
|
||||
<div className="w-full h-screen flex justify-center items-center bg-black">
|
||||
<iframe
|
||||
src={`/UnityBuild/${myActiveBet.id}/index.html?betId=${myActiveBet.id}&owner=${myActiveBet.owner_id}&joiner=${myActiveBet.joiner_id}&address=${myActiveBet.address}&uid=${user?.id}&pubkey=${solWallet?.address}`} // Change this to the actual path of your Unity WebGL build
|
||||
ref={iframeRef}
|
||||
src={`/UnityBuild/${myActiveBet.id}/index.html?betId=${myActiveBet.id}&owner=${myActiveBet.owner_id}&joiner=${myActiveBet.joiner_id}&address=${myActiveBet.address}&uid=${user?.id}&pubkey=${solWallet?.address}&wager=${myActiveBet.wager * 1e8}`}
|
||||
className="w-full h-full"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Render the original UI when no active bet
|
||||
<section className="flex flex-col items-center text-center py-16">
|
||||
<Image
|
||||
src="/duelfiassets/Playing on Arcade Machine no BG.png"
|
||||
|
|
@ -116,6 +341,15 @@ export default function HeroSection() {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{rematch && (
|
||||
<RematchModal
|
||||
isOwner= {lastActiveBet?.owner_id === user?.id}
|
||||
inProgress={rematchInProgress}
|
||||
hasError={rematchTxError}
|
||||
wallets={solWallet!}
|
||||
/>
|
||||
)}
|
||||
|
||||
<GameModal isOpen={isGameModalOpen} onClose={() => setIsGameModalOpen(false)} />
|
||||
<HowItWorksModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Bet } from "../types/Bet";
|
|||
import { fetchUserById } from "@/shared/data_fetcher";
|
||||
import { toast } from "sonner";
|
||||
import { EXPLORER_TX_TEMPLATE } from "@/data/shared";
|
||||
import { WAGER_PRIZE_MULT } from "@/shared/constants";
|
||||
|
||||
interface GameModalProps {
|
||||
bets: Bet[];
|
||||
|
|
@ -120,7 +121,7 @@ export default function YourGames({ bets }: GameModalProps) {
|
|||
|
||||
<div className="flex justify-between text-xs font-mono py-1">
|
||||
<p className="text-white">{bet.wager} SOL</p>
|
||||
<p className="text-white">{(bet.wager * 2 * 0.95).toFixed(2)} SOL</p>
|
||||
<p className="text-white">{(bet.wager * 2 * WAGER_PRIZE_MULT).toFixed(2)} SOL</p>
|
||||
</div>
|
||||
|
||||
{bet.ownerProfile && (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { usePrivy, useSolanaWallets } from "@privy-io/react-auth";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
|
|
@ -7,12 +7,14 @@ import { toast } from "sonner";
|
|||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { CLUSTER_URL } from "@/data/shared";
|
||||
import { useFundWallet } from "@privy-io/react-auth/solana";
|
||||
import GameHistoryModal from "./GameHistory";
|
||||
|
||||
export default function PrivyButton() {
|
||||
const { login, logout, user, linkTwitter, unlinkTwitter } = usePrivy();
|
||||
const { fundWallet} = useFundWallet();
|
||||
const { wallets } = useSolanaWallets();
|
||||
const { fundWallet } = useFundWallet();
|
||||
const { wallets, ready } = useSolanaWallets();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [solWallet, setSolWallet] = useState("");
|
||||
const [solBalance, setSolBalance] = useState("--");
|
||||
|
||||
|
|
@ -24,7 +26,7 @@ export default function PrivyButton() {
|
|||
const [newUsername, setNewUsername] = useState("");
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const updateSolWallet = ()=>{
|
||||
const updateSolWallet = () => {
|
||||
wallets.forEach((wallet) => {
|
||||
if (wallet.type === "solana") {
|
||||
setSolWallet(wallet.address);
|
||||
|
|
@ -47,6 +49,15 @@ export default function PrivyButton() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
fetchSolBalance();
|
||||
}, 5000); // 5000 milliseconds = 5 seconds
|
||||
|
||||
// Cleanup function to clear the interval on unmount or when `ready` changes
|
||||
return () => clearInterval(intervalId);
|
||||
}, [ready]);
|
||||
|
||||
const saveProfileChanges = async () => {
|
||||
if (!user) {
|
||||
toast.error("User not found!");
|
||||
|
|
@ -210,7 +221,15 @@ export default function PrivyButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<GameHistoryModal
|
||||
userId={user?.id}
|
||||
isOpen={showHistory}
|
||||
onClose={() => setShowHistory(false)
|
||||
}
|
||||
/>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
|
|
@ -224,6 +243,44 @@ export default function PrivyButton() {
|
|||
{solWallet?.slice(0, 6)}...{solWallet?.slice(-4)}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<p className="text-s font-mono text-white">
|
||||
<Image
|
||||
src="/duelfiassets/solana logo.png"
|
||||
alt="SOL"
|
||||
width={25}
|
||||
height={25}
|
||||
className="inline mx-2"
|
||||
/>
|
||||
{solBalance}
|
||||
</p>
|
||||
|
||||
{/* History button */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setShowHistory(true)}
|
||||
title="View Game History"
|
||||
className="hover:text-orange-400 transition"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={customLogin}
|
||||
|
|
@ -331,7 +388,7 @@ export default function PrivyButton() {
|
|||
<p className="text-sm mb-3">{solBalance} SOL</p>
|
||||
|
||||
<button
|
||||
onClick={()=>{fundWallet(solWallet, {})}}
|
||||
onClick={() => { fundWallet(solWallet, {}) }}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold py-2 rounded-xl transition"
|
||||
>
|
||||
Fund Wallet
|
||||
|
|
@ -371,7 +428,7 @@ export default function PrivyButton() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
60
src/components/RematchModal.tsx
Normal file
60
src/components/RematchModal.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"use client";
|
||||
import { Bet } from "@/types/Bet";
|
||||
import React, { useEffect } from "react";
|
||||
import { fetchOpenBets } from "../shared/solana_helpers"; // adjust path as needed
|
||||
import { ConnectedSolanaWallet } from "@privy-io/react-auth"; // import your wallet type
|
||||
|
||||
interface RematchModalProps {
|
||||
isOwner: boolean;
|
||||
inProgress: boolean;
|
||||
hasError: boolean;
|
||||
wallets: ConnectedSolanaWallet;
|
||||
}
|
||||
|
||||
export const RematchModal: React.FC<RematchModalProps> = ({
|
||||
isOwner,
|
||||
inProgress,
|
||||
hasError,
|
||||
wallets
|
||||
} : RematchModalProps) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-10 flex items-center justify-center z-50">
|
||||
<div className="bg-[rgb(30,30,30)] rounded-xl shadow-lg p-6 max-w-md w-full text-center space-y-4">
|
||||
<h2 className="text-2xl font-bold">Starting Rematch</h2>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<p>Waiting for transaction</p>
|
||||
{/* <button
|
||||
onClick={onCreate}
|
||||
disabled={inProgress}
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white font-semibold py-2 px-6 rounded-xl disabled:opacity-50"
|
||||
>
|
||||
{inProgress ? "Creating..." : "Create Rematch"}
|
||||
</button> */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>Your opponent is setting up a rematch. Ready to join?</p>
|
||||
{/* <button
|
||||
onClick={onJoin}
|
||||
disabled={inProgress}
|
||||
className="bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-6 rounded-xl disabled:opacity-50"
|
||||
>
|
||||
{inProgress ? "Joining..." : "Join Rematch"}
|
||||
</button> */}
|
||||
</>
|
||||
)}
|
||||
{hasError && (
|
||||
<button
|
||||
// onClick={onRetry}
|
||||
className="bg-red-500 text-white py-2 px-4 mt-4 rounded-md"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -82,7 +82,7 @@ export default function YourGames({bets}:GameModalProps) {
|
|||
if (!game) return null;
|
||||
|
||||
return (
|
||||
<div key={bet.id} className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300">
|
||||
<div key={bet.address} className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300">
|
||||
{/* Game Thumbnail */}
|
||||
<div className="relative w-full h-40 overflow-hidden rounded-xl">
|
||||
<Image
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Game } from "@/types/Game";
|
||||
|
||||
export const games = [
|
||||
{
|
||||
id: "tetris",
|
||||
|
|
@ -19,3 +21,6 @@ export const games = [
|
|||
},
|
||||
];
|
||||
|
||||
export function GetGameByID(id:string):Game | undefined{
|
||||
return games.find((game)=> game.id == id);
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
import { Commitment, PublicKey } from "@solana/web3.js";
|
||||
|
||||
export const FEE_COLLECTOR_PUBKEY = new PublicKey("cocD4r4yNpHxPq7CzUebxEMyLki3X4d2Y3HcTX5ptUc");
|
||||
export const WAGER_PRIZE_MULT = 0.95;
|
||||
|
||||
export const CONFIRMATION_THRESHOLD:Commitment = 'processed';
|
||||
|
|
@ -7,7 +7,7 @@ import idl from "../idl/bets_idl.json";
|
|||
import { Bet } from "@/types/Bet";
|
||||
import { toast } from "sonner";
|
||||
import { Game } from "@/types/Game";
|
||||
import { FEE_COLLECTOR_PUBKEY } from "./constants";
|
||||
import { CONFIRMATION_THRESHOLD, FEE_COLLECTOR_PUBKEY } from "./constants";
|
||||
|
||||
export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet[]> => {
|
||||
try {
|
||||
|
|
@ -20,7 +20,7 @@ export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet
|
|||
signAllTransactions: wallets.signAllTransactions,
|
||||
};
|
||||
const provider = new AnchorProvider(connection, wallet, {
|
||||
preflightCommitment: "confirmed",
|
||||
preflightCommitment: CONFIRMATION_THRESHOLD,
|
||||
});
|
||||
|
||||
const program = new Program<Bets>(idl, provider);
|
||||
|
|
@ -35,29 +35,63 @@ export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet
|
|||
const formattedBets = await Promise.all(
|
||||
bet_list.bets.map(async (bet) => {
|
||||
const betAcc = await program.account.betVault.fetch(bet);
|
||||
console.log(betAcc.gameId);
|
||||
// console.log(betAcc.gameId);
|
||||
return {
|
||||
address: bet.toString(),
|
||||
id: betAcc.gameId,
|
||||
owner: betAcc.owner.toBase58(),
|
||||
owner_id:betAcc.ownerId,
|
||||
owner_id: betAcc.ownerId,
|
||||
joiner: betAcc.joiner ? betAcc.joiner.toBase58() : "Open",
|
||||
joiner_id:betAcc.joinerId,
|
||||
joiner_id: betAcc.joinerId,
|
||||
wager: betAcc.wager.toNumber() / LAMPORTS_PER_SOL
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`Got ${formattedBets.length} bets`);
|
||||
// console.log(`Got ${formattedBets.length} bets`);
|
||||
|
||||
return formattedBets;
|
||||
} catch (error) {
|
||||
console.error("Error fetching open bets:", error);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
};
|
||||
|
||||
export async function closeBet(wallets: ConnectedSolanaWallet, betId: string): Promise<string> {
|
||||
export async function getVaultByAddress(wallets: ConnectedSolanaWallet, address: string): Promise<Bet | undefined> {
|
||||
try {
|
||||
if (!wallets) return undefined;
|
||||
|
||||
const connection = new Connection(CLUSTER_URL);
|
||||
const wallet = {
|
||||
publicKey: new PublicKey(wallets.address),
|
||||
signTransaction: wallets.signTransaction,
|
||||
signAllTransactions: wallets.signAllTransactions,
|
||||
};
|
||||
const provider = new AnchorProvider(connection, wallet, {
|
||||
preflightCommitment: CONFIRMATION_THRESHOLD,
|
||||
});
|
||||
|
||||
const program = new Program<Bets>(idl, provider);
|
||||
|
||||
// Extract required bet data
|
||||
const betAcc = await program.account.betVault.fetch(address);
|
||||
// console.log(betAcc.gameId);
|
||||
return {
|
||||
address: address.toString(),
|
||||
id: betAcc.gameId,
|
||||
owner: betAcc.owner.toBase58(),
|
||||
owner_id: betAcc.ownerId,
|
||||
joiner: betAcc.joiner ? betAcc.joiner.toBase58() : "Open",
|
||||
joiner_id: betAcc.joinerId,
|
||||
wager: betAcc.wager.toNumber() / LAMPORTS_PER_SOL
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching open bets:", error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function closeBet(wallets: ConnectedSolanaWallet, betId: string): Promise<string> {
|
||||
try {
|
||||
const connection = new Connection(CLUSTER_URL);
|
||||
const wallet = {
|
||||
|
|
@ -66,7 +100,7 @@ export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet
|
|||
signAllTransactions: wallets.signAllTransactions,
|
||||
};
|
||||
const provider = new AnchorProvider(connection, wallet, {
|
||||
preflightCommitment: "confirmed",
|
||||
preflightCommitment: CONFIRMATION_THRESHOLD,
|
||||
});
|
||||
|
||||
const program = new Program<Bets>(idl, provider);
|
||||
|
|
@ -121,7 +155,7 @@ export const fetchOpenBets = async (wallets: ConnectedSolanaWallet): Promise<Bet
|
|||
return "";
|
||||
}
|
||||
|
||||
export async function createBet(wallets:ConnectedSolanaWallet, uid:string,selectedPrice:number,selectedGame:Game):Promise<string>{
|
||||
export async function createBet(wallets: ConnectedSolanaWallet, uid: string, selectedPrice: number, selectedGame: string, get_account_address: boolean): Promise<string> {
|
||||
const connection = new Connection(CLUSTER_URL);
|
||||
const wallet = {
|
||||
publicKey: new PublicKey(wallets.address),
|
||||
|
|
@ -129,24 +163,24 @@ export async function createBet(wallets:ConnectedSolanaWallet, uid:string,select
|
|||
signAllTransactions: wallets.signAllTransactions,
|
||||
};
|
||||
const provider = new AnchorProvider(connection, wallet, {
|
||||
preflightCommitment: "confirmed",
|
||||
preflightCommitment: CONFIRMATION_THRESHOLD,
|
||||
});
|
||||
|
||||
const program = new Program<Bets>(idl, provider);
|
||||
|
||||
try {
|
||||
toast.loading("Creating bet...");
|
||||
const nonce = getRandomInt(100000000);
|
||||
const [bet_list_pda] = await PublicKey.findProgramAddress(
|
||||
[Buffer.from("bets_list")],
|
||||
program.programId
|
||||
);
|
||||
const connection = new Connection(CLUSTER_URL, "confirmed");
|
||||
|
||||
console.log(`bets list : ${bet_list_pda}`);
|
||||
|
||||
// Create transaction
|
||||
const tx = await program.methods
|
||||
.createBet(new BN(selectedPrice * 1000000000),uid, selectedGame.id, new BN(nonce))
|
||||
.createBet(new BN(selectedPrice * 1000000000), uid, selectedGame, new BN(nonce))
|
||||
.accounts({
|
||||
betsList: bet_list_pda,
|
||||
})
|
||||
|
|
@ -158,14 +192,31 @@ export async function createBet(wallets:ConnectedSolanaWallet, uid:string,select
|
|||
// Sign transaction with Privy
|
||||
const signedTx = await wallet.signTransaction(tx);
|
||||
|
||||
// Send transaction// Replace with correct RPC endpoint
|
||||
// Send transaction
|
||||
const txId = await connection.sendRawTransaction(signedTx.serialize());
|
||||
console.log(`Transaction sent: ${txId}`);
|
||||
|
||||
console.log(`Transaction ID: ${txId}`);
|
||||
toast.success("Bet created successfully");
|
||||
console.log(`Transaction confirmed: ${txId}`);
|
||||
if (get_account_address) {
|
||||
const gameIdBytes = Buffer.from(selectedGame); // selectedGame is a string (game_id)
|
||||
const nonceBytes = new BN(nonce).toArrayLike(Buffer, "le", 8); // _nonce.to_le_bytes()
|
||||
const [betVaultPda] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
Buffer.from("bet_vault"),
|
||||
new PublicKey(wallets.address).toBuffer(), // payer
|
||||
gameIdBytes,
|
||||
nonceBytes,
|
||||
],
|
||||
program.programId
|
||||
);
|
||||
|
||||
return betVaultPda.toString();
|
||||
} else {
|
||||
return txId;
|
||||
} catch (error:unknown) {
|
||||
|
||||
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.dismiss();
|
||||
const errorMessage = String(error); // Converts error to string safely
|
||||
|
||||
if (errorMessage.includes("already in use")) {
|
||||
|
|
@ -179,7 +230,7 @@ export async function createBet(wallets:ConnectedSolanaWallet, uid:string,select
|
|||
return "";
|
||||
}
|
||||
|
||||
export async function joinBet(wallets:ConnectedSolanaWallet, uid:string, gameId:string, address:string):Promise<string>{
|
||||
export async function joinBet(wallets: ConnectedSolanaWallet, uid: string, gameId: string, address: string): Promise<string> {
|
||||
const connection = new Connection(CLUSTER_URL);
|
||||
const wallet = {
|
||||
publicKey: new PublicKey(wallets.address),
|
||||
|
|
@ -187,7 +238,7 @@ export async function joinBet(wallets:ConnectedSolanaWallet, uid:string, gameId:
|
|||
signAllTransactions: wallets.signAllTransactions,
|
||||
};
|
||||
const provider = new AnchorProvider(connection, wallet, {
|
||||
preflightCommitment: "confirmed",
|
||||
preflightCommitment: CONFIRMATION_THRESHOLD,
|
||||
});
|
||||
|
||||
const program = new Program<Bets>(idl, provider);
|
||||
|
|
@ -197,14 +248,14 @@ export async function joinBet(wallets:ConnectedSolanaWallet, uid:string, gameId:
|
|||
[Buffer.from("bets_list")],
|
||||
program.programId
|
||||
);
|
||||
const connection = new Connection(CLUSTER_URL, "confirmed");
|
||||
const connection = new Connection(CLUSTER_URL, CONFIRMATION_THRESHOLD);
|
||||
const betVaultPubkey = new PublicKey(address);
|
||||
|
||||
console.log(`bets list : ${bet_list_pda}`);
|
||||
|
||||
// Create transaction
|
||||
const tx = await program.methods
|
||||
.joinBet(uid,gameId)
|
||||
.joinBet(uid, gameId)
|
||||
.accounts({
|
||||
betVault: betVaultPubkey,
|
||||
})
|
||||
|
|
@ -221,7 +272,7 @@ export async function joinBet(wallets:ConnectedSolanaWallet, uid:string, gameId:
|
|||
|
||||
console.log(`Transaction ID: ${txId}`);
|
||||
return txId;
|
||||
} catch (error:unknown) {
|
||||
} catch (error: unknown) {
|
||||
|
||||
|
||||
// const errorMessage = String(error); // Converts error to string safely
|
||||
|
|
@ -233,6 +284,6 @@ export async function joinBet(wallets:ConnectedSolanaWallet, uid:string, gameId:
|
|||
return "";
|
||||
}
|
||||
|
||||
function getRandomInt(max:number):number {
|
||||
function getRandomInt(max: number): number {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
10
src/types/GameHistory.ts
Normal file
10
src/types/GameHistory.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
interface GameHistory {
|
||||
address: string;
|
||||
master_score: string;
|
||||
client_score: string;
|
||||
winner: string;
|
||||
wager: string;
|
||||
master_id: string;
|
||||
client_id: string;
|
||||
game:string;
|
||||
}
|
||||
9
src/types/Submission.tsx
Normal file
9
src/types/Submission.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export interface Submission{
|
||||
username: string;
|
||||
master_score: number;
|
||||
client_score: number;
|
||||
leaderboard: any;
|
||||
master_id:string;
|
||||
client_id:string;
|
||||
winner:string;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user