rematch proto

This commit is contained in:
Sewmina 2025-04-08 12:40:27 +05:30
parent c37f8f7457
commit de435d89aa
20 changed files with 1139 additions and 337 deletions

115
package-lock.json generated
View File

@ -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",

View File

@ -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",

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

File diff suppressed because one or more lines are too long

View File

@ -1,122 +1,133 @@
<!DOCTYPE html>
<html lang="en-us">
<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>
<div id="unity-container" class="unity-desktop">
<canvas id="unity-canvas" width=1280 height=720 tabindex="-1"></canvas>
<div id="unity-loading-bar">
<div id="unity-logo"></div>
<div id="unity-progress-bar-empty">
<div id="unity-progress-bar-full"></div>
</div>
</div>
<div id="unity-warning"> </div>
<div id="unity-footer">
<div id="unity-webgl-logo"></div>
<div id="unity-fullscreen-button"></div>
<div id="unity-build-title">TetrisMP</div>
<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>
<div id="unity-container" class="unity-desktop">
<canvas id="unity-canvas" width=1280 height=720 tabindex="-1"></canvas>
<div id="unity-loading-bar">
<div id="unity-logo"></div>
<div id="unity-progress-bar-empty">
<div id="unity-progress-bar-full"></div>
</div>
</div>
<script>
<div id="unity-warning"> </div>
<div id="unity-footer">
<div id="unity-webgl-logo"></div>
<div id="unity-fullscreen-button"></div>
<div id="unity-build-title">TetrisMP</div>
</div>
</div>
<script>
var container = document.querySelector("#unity-container");
var canvas = document.querySelector("#unity-canvas");
var loadingBar = document.querySelector("#unity-loading-bar");
var progressBarFull = document.querySelector("#unity-progress-bar-full");
var fullscreenButton = document.querySelector("#unity-fullscreen-button");
var warningBanner = document.querySelector("#unity-warning");
var container = document.querySelector("#unity-container");
var canvas = document.querySelector("#unity-canvas");
var loadingBar = document.querySelector("#unity-loading-bar");
var progressBarFull = document.querySelector("#unity-progress-bar-full");
var fullscreenButton = document.querySelector("#unity-fullscreen-button");
var warningBanner = document.querySelector("#unity-warning");
// Shows a temporary message banner/ribbon for a few seconds, or
// a permanent error message on top of the canvas if type=='error'.
// If type=='warning', a yellow highlight color is used.
// Modify or remove this function to customize the visually presented
// way that non-critical warnings and error messages are presented to the
// user.
function unityShowBanner(msg, type) {
function updateBannerVisibility() {
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
}
var div = document.createElement('div');
div.innerHTML = msg;
warningBanner.appendChild(div);
if (type == 'error') div.style = 'background: red; padding: 10px;';
else {
if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
setTimeout(function() {
warningBanner.removeChild(div);
updateBannerVisibility();
}, 5000);
}
updateBannerVisibility();
// Shows a temporary message banner/ribbon for a few seconds, or
// a permanent error message on top of the canvas if type=='error'.
// If type=='warning', a yellow highlight color is used.
// Modify or remove this function to customize the visually presented
// way that non-critical warnings and error messages are presented to the
// user.
function unityShowBanner(msg, type) {
function updateBannerVisibility() {
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
}
var div = document.createElement('div');
div.innerHTML = msg;
warningBanner.appendChild(div);
if (type == 'error') div.style = 'background: red; padding: 10px;';
else {
if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
setTimeout(function () {
warningBanner.removeChild(div);
updateBannerVisibility();
}, 5000);
}
updateBannerVisibility();
}
var buildUrl = "Build";
var loaderUrl = buildUrl + "/prod.loader.js";
var config = {
dataUrl: buildUrl + "/prod.data",
frameworkUrl: buildUrl + "/prod.framework.js",
codeUrl: buildUrl + "/prod.wasm",
streamingAssetsUrl: "StreamingAssets",
companyName: "DefaultCompany",
productName: "TetrisMP",
productVersion: "1.0",
showBanner: unityShowBanner,
};
// 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
// happening inside the engine, and you would instead like to size up
// the canvas DOM size and WebGL render target sizes yourself.
// config.matchWebGLToCanvasSize = false;
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
// Mobile device style: fill the whole browser client area with the game canvas:
var meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
document.getElementsByTagName('head')[0].appendChild(meta);
container.className = "unity-mobile";
canvas.className = "unity-mobile";
// To lower canvas resolution on mobile devices to gain some
// performance, uncomment the following line:
// config.devicePixelRatio = 1;
var buildUrl = "Build";
var loaderUrl = buildUrl + "/prod.loader.js";
var config = {
dataUrl: buildUrl + "/prod.data",
frameworkUrl: buildUrl + "/prod.framework.js",
codeUrl: buildUrl + "/prod.wasm",
streamingAssetsUrl: "StreamingAssets",
companyName: "DefaultCompany",
productName: "TetrisMP",
productVersion: "1.0",
showBanner: unityShowBanner,
};
/*UNITY BRIDGE*/
function sendMessageToReact(message) {
if (window.parent) {
window.parent.postMessage(message, "*"); // Replace "*" with your React app's origin for security
} else {
// Desktop style: Render the game canvas in a window that can be maximized to fullscreen:
canvas.style.width = "1280px";
canvas.style.height = "720px";
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
// happening inside the engine, and you would instead like to size up
// the canvas DOM size and WebGL render target sizes yourself.
// config.matchWebGLToCanvasSize = false;
loadingBar.style.display = "block";
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
// Mobile device style: fill the whole browser client area with the game canvas:
var script = document.createElement("script");
script.src = loaderUrl;
script.onload = () => {
createUnityInstance(canvas, config, (progress) => {
progressBarFull.style.width = 100 * progress + "%";
}).then((unityInstance) => {
loadingBar.style.display = "none";
fullscreenButton.onclick = () => {
unityInstance.SetFullscreen(1);
};
}).catch((message) => {
alert(message);
});
};
var meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
document.getElementsByTagName('head')[0].appendChild(meta);
container.className = "unity-mobile";
canvas.className = "unity-mobile";
document.body.appendChild(script);
// To lower canvas resolution on mobile devices to gain some
// performance, uncomment the following line:
// config.devicePixelRatio = 1;
</script>
</body>
</html>
} else {
// Desktop style: Render the game canvas in a window that can be maximized to fullscreen:
canvas.style.width = "1280px";
canvas.style.height = "720px";
}
loadingBar.style.display = "block";
var script = document.createElement("script");
script.src = loaderUrl;
script.onload = () => {
createUnityInstance(canvas, config, (progress) => {
progressBarFull.style.width = 100 * progress + "%";
}).then((unityInstance) => {
loadingBar.style.display = "none";
fullscreenButton.onclick = () => {
unityInstance.SetFullscreen(1);
};
}).catch((message) => {
alert(message);
});
};
document.body.appendChild(script);
</script>
</body>
</html>

View File

@ -93,4 +93,64 @@ 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 */
}

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

View File

@ -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!`, {

View File

@ -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() {
if (!ready || wallets.length === 0) return;
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}`);
// Find the wallet to use (either Solana wallet or the first available wallet)
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;
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 fetchedBets = await fetchOpenBets(wallet);
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
// Set the state for filtered bets and active bet
const activeBet = filledBets.find((bet) => bet.owner_id === user?.id || bet.joiner_id === user?.id);
if(rematch){
setMyActiveBet(undefined);
return;
}
setBets(filteredBets);
setMyActiveBet(activeBet);
if (!myActiveBet && lastActiveBet?.address !== activeBet?.address) {
setMyActiveBet(activeBet);
setLastActiveBet(activeBet);
}
};
const handleCreateRematch = async () => {
console.log('Creating rematch...');
if (!lastActiveBet || !solWallet) return;
// If needed, you can also use the filledBets list for something else
}
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)} />
</>

View File

@ -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 && (

View File

@ -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,18 +49,27 @@ 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!");
return;
}
const updateUrl = `https://vps.playpoolstudios.com/duelfi/api/update_profile.php?id=${user.id}&username=${username}&bio=${bio}`;
try {
const response = await fetch(updateUrl);
const data = await response.text();
if (data == "0") {
toast.success("Profile updated successfully!");
} else {
@ -139,36 +150,36 @@ export default function PrivyButton() {
if (file) {
const reader = new FileReader();
reader.onloadend = async () => {
try {
// Ensure the user is authenticated and the privy_id is available
if (!user?.id) {
toast.error('No Privy ID found!');
return;
}
// Prepare the form data
const formData = new FormData();
formData.append('file', file);
formData.append('privy_id', user.id); // Append the privy_id
// Upload the avatar image to your server
const uploadResponse = await fetch('https://vps.playpoolstudios.com/duelfi/api/upload_profile_picture.php', {
method: 'POST',
body: formData,
});
const uploadData = await uploadResponse.json();
console.log(uploadData);
if (uploadData.success) {
// Get the image URL from the response (assuming it returns the image path)
const imageUrl = uploadData.imageUrl;
// Update the avatar state and database
setAvatar(imageUrl);
const updatePicUrlApi = `https://vps.playpoolstudios.com/duelfi/api/update_x_pic_url.php?id=${user?.id}&url=${imageUrl}`;
await fetch(updatePicUrlApi);
toast.success('Profile picture uploaded successfully!');
} else {
@ -210,20 +221,66 @@ export default function PrivyButton() {
return (
<>
<GameHistoryModal
userId={user?.id}
isOpen={showHistory}
onClose={() => setShowHistory(false)
}
/>
{user ? (
<button
onClick={() => {
setIsModalOpen(true);
fetchSolBalance();
fetchUserData();
}}
className="bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black px-6 py-3 rounded-md transition duration-300 hover:scale-105"
>
<span className="font-bold">Connected</span>
<p className="text-xs font-mono text-gray-700">
{solWallet?.slice(0, 6)}...{solWallet?.slice(-4)}
</p>
</button>
<>
<div className="flex items-center justify-between gap-4">
<button
onClick={() => {
setIsModalOpen(true);
fetchSolBalance();
fetchUserData();
}}
className="bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-black px-6 py-3 rounded-md transition duration-300 hover:scale-105"
>
<span className="font-bold">Connected</span>
<p className="text-xs font-mono text-gray-700">
{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
@ -353,25 +410,25 @@ export default function PrivyButton() {
{/* Username Claim Modal */}
{isUsernameClaimModalOpen && (
<div className="fixed inset-0 bg-black/70 flex justify-center items-center z-50">
<div className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg space-y-6">
<h2 className="text-2xl font-bold">Claim Your Username</h2>
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
className="w-full bg-[rgb(10,10,10)] text-white p-2 rounded-md"
placeholder="Enter your new username"
/>
<button
onClick={handleUsernameClaim}
className="w-full bg-[rgb(248,144,22)] hover:bg-orange-400 text-white px-4 py-2 rounded-md transition duration-500 hover:scale-105"
>
Claim
</button>
</div>
</div>
)}
<div className="fixed inset-0 bg-black/70 flex justify-center items-center z-50">
<div className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg space-y-6">
<h2 className="text-2xl font-bold">Claim Your Username</h2>
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
className="w-full bg-[rgb(10,10,10)] text-white p-2 rounded-md"
placeholder="Enter your new username"
/>
<button
onClick={handleUsernameClaim}
className="w-full bg-[rgb(248,144,22)] hover:bg-orange-400 text-white px-4 py-2 rounded-md transition duration-500 hover:scale-105"
>
Claim
</button>
</div>
</div>
)}
</>
);

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

View File

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

View File

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

View File

@ -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 FEE_COLLECTOR_PUBKEY = new PublicKey("cocD4r4yNpHxPq7CzUebxEMyLki3X4d2Y3HcTX5ptUc");
export const WAGER_PRIZE_MULT = 0.95;
export const CONFIRMATION_THRESHOLD:Commitment = 'processed';

View File

@ -7,146 +7,180 @@ 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 {
if (!wallets) return [];
try {
if (!wallets) return [];
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: "confirmed",
});
const program = new Program<Bets>(idl, provider);
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
);
// Fetch all open bet accounts
const bet_list = await program.account.betsList.fetch(bet_list_pda);
// Extract required bet data
const formattedBets = await Promise.all(
bet_list.bets.map(async (bet) => {
const betAcc = await program.account.betVault.fetch(bet);
console.log(betAcc.gameId);
return {
address: bet.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
};
})
);
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> {
try {
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: "confirmed",
});
const program = new Program<Bets>(idl, provider);
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
);
// Fetch the bet list
const betList = await program.account.betsList.fetch(bet_list_pda);
let chosenBet: PublicKey | null = null;
for (const bet of betList.bets) {
const betAcc = await program.account.betVault.fetch(bet);
if (betAcc.owner.toBase58() === wallets.address && betAcc.gameId.toString() === betId) {
chosenBet = bet;
break;
}
}
if (!chosenBet) {
console.error("Bet not found or not owned by the user");
return "";
}
const winner = new PublicKey(wallets.address);
// Execute the closeBet transaction
const tx = await program.methods
.closeBet(winner)
.accounts({
betVault: chosenBet,
betsList: bet_list_pda,
winner: winner,
feeWallet: FEE_COLLECTOR_PUBKEY
})
.transaction();
tx.feePayer = new PublicKey(wallets.address);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
// Sign transaction with Privy
const signedTx = await wallet.signTransaction(tx);
// Send transaction// Replace with correct RPC endpoint
const txId = await connection.sendRawTransaction(signedTx.serialize());
console.log(`Transaction: ${tx}`);
return txId;
} catch (error) {
console.error("Error closing bet:", error);
}
return "";
}
export async function createBet(wallets:ConnectedSolanaWallet, uid:string,selectedPrice:number,selectedGame:Game):Promise<string>{
const connection = new Connection(CLUSTER_URL);
const wallet = {
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);
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
);
// Fetch all open bet accounts
const bet_list = await program.account.betsList.fetch(bet_list_pda);
// Extract required bet data
const formattedBets = await Promise.all(
bet_list.bets.map(async (bet) => {
const betAcc = await program.account.betVault.fetch(bet);
// console.log(betAcc.gameId);
return {
address: bet.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
};
})
);
// console.log(`Got ${formattedBets.length} bets`);
return formattedBets;
} catch (error) {
console.error("Error fetching open bets:", error);
}
return [];
};
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 = {
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);
const [bet_list_pda] = await PublicKey.findProgramAddress(
[Buffer.from("bets_list")],
program.programId
);
// Fetch the bet list
const betList = await program.account.betsList.fetch(bet_list_pda);
let chosenBet: PublicKey | null = null;
for (const bet of betList.bets) {
const betAcc = await program.account.betVault.fetch(bet);
if (betAcc.owner.toBase58() === wallets.address && betAcc.gameId.toString() === betId) {
chosenBet = bet;
break;
}
}
if (!chosenBet) {
console.error("Bet not found or not owned by the user");
return "";
}
const winner = new PublicKey(wallets.address);
// Execute the closeBet transaction
const tx = await program.methods
.closeBet(winner)
.accounts({
betVault: chosenBet,
betsList: bet_list_pda,
winner: winner,
feeWallet: FEE_COLLECTOR_PUBKEY
})
.transaction();
tx.feePayer = new PublicKey(wallets.address);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
// Sign transaction with Privy
const signedTx = await wallet.signTransaction(tx);
// Send transaction// Replace with correct RPC endpoint
const txId = await connection.sendRawTransaction(signedTx.serialize());
console.log(`Transaction: ${tx}`);
return txId;
} catch (error) {
console.error("Error closing bet:", error);
}
return "";
}
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),
signTransaction: wallets.signTransaction,
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}`);
return txId;
} catch (error:unknown) {
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) {
toast.dismiss();
const errorMessage = String(error); // Converts error to string safely
if (errorMessage.includes("already in use")) {
@ -179,32 +230,32 @@ 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),
signTransaction: wallets.signTransaction,
signAllTransactions: wallets.signAllTransactions,
publicKey: new PublicKey(wallets.address),
signTransaction: wallets.signTransaction,
signAllTransactions: wallets.signAllTransactions,
};
const provider = new AnchorProvider(connection, wallet, {
preflightCommitment: "confirmed",
preflightCommitment: CONFIRMATION_THRESHOLD,
});
const program = new Program<Bets>(idl, provider);
try {
const [bet_list_pda] = await PublicKey.findProgramAddress(
[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,18 +272,18 @@ 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
toast.error("Failed to create bet.");
console.error(error);
}
return "";
}
function getRandomInt(max:number):number {
function getRandomInt(max: number): number {
return Math.floor(Math.random() * max);
}

10
src/types/GameHistory.ts Normal file
View 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
View 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;
}