Merge remote-tracking branch 'origin/dev'

This commit is contained in:
Sewmina 2025-06-02 15:29:43 +00:00
commit 1735caa649
21 changed files with 807 additions and 30 deletions

137
package-lock.json generated
View File

@ -21,6 +21,7 @@
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-toastify": "^11.0.5",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.3",
"viem": "^2.24.2"
},
@ -2276,6 +2277,12 @@
"integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==",
"license": "MIT"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@solana/buffer-layout": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz",
@ -5075,6 +5082,66 @@
"once": "^1.4.0"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@ -9243,6 +9310,68 @@
"is-arrayish": "^0.3.1"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/sonic-boom": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz",
@ -10438,6 +10567,14 @@
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",

View File

@ -22,6 +22,7 @@
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-toastify": "^11.0.5",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.3",
"viem": "^2.24.2"
},

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="28" fill="none"><path d="M 9.03 0.103 C 9.286 -0.03 9.59 -0.035 9.85 0.09 C 10.11 0.214 10.297 0.453 10.355 0.736 L 11.942 8.711 C 12.065 9.329 11.352 9.77 10.847 9.387 L 10.412 9.057 C 9.756 8.559 9.071 8.098 8.362 7.678 L 4.411 16.262 C 4.077 16.986 3.904 17.774 3.904 18.571 C 3.904 21.637 6.408 24.123 9.496 24.123 L 15.846 24.123 C 18.193 24.123 20.096 22.234 20.096 19.904 C 20.096 17.574 18.193 15.685 15.846 15.685 L 12.402 15.685 C 11.886 15.687 11.391 15.484 11.025 15.12 C 10.659 14.757 10.452 14.263 10.45 13.747 C 10.45 12.677 11.324 11.808 12.402 11.808 L 16.19 11.808 C 16.888 11.811 17.559 11.536 18.054 11.044 C 18.55 10.552 18.829 9.884 18.832 9.186 C 18.83 8.488 18.55 7.819 18.054 7.327 C 17.559 6.835 16.888 6.56 16.19 6.563 L 15.846 6.563 C 15.33 6.565 14.835 6.362 14.469 5.998 C 14.103 5.635 13.896 5.141 13.894 4.625 C 13.894 3.554 14.768 2.686 15.846 2.686 L 16.19 2.686 C 19.805 2.686 22.736 5.596 22.736 9.186 C 22.737 10.832 22.11 12.417 20.981 13.615 C 22.89 15.145 24 17.459 24 19.905 C 24 24.374 20.35 28 15.847 28 L 9.496 28 C 4.251 28 0 23.778 0 18.57 C 0 17.218 0.293 15.88 0.86 14.65 L 4.842 6 C 4.124 5.735 3.393 5.508 2.652 5.32 L 1.896 5.127 C 1.62 5.058 1.416 4.825 1.383 4.543 C 1.35 4.26 1.496 3.987 1.749 3.857 Z" fill="rgb(255, 255, 255)"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,16 @@
<svg width="512" height="512" viewBox="0 0 506.623 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_207_2675)">
<path d="M506.621 253.985C507.254 394.894 394.35 509.631 254.463 510.268C114.557 510.906 0.635972 397.194 0.00265142 256.286C-0.630669 115.377 112.273 0.640368 252.179 0.00251182C392.066 -0.616016 505.987 113.077 506.621 253.985Z" fill="#FFE866"/>
<path d="M381.781 163.839C363.395 158.485 344.357 150.869 325.05 143.195C323.937 138.325 319.658 132.255 310.983 124.814C298.374 113.796 274.692 114.086 254.234 118.957C231.645 113.603 209.326 111.689 187.908 116.869C12.7658 165.482 112.063 284.026 47.752 403.228C56.9064 422.77 157.872 516.658 298.24 506.233C298.24 506.233 249.436 388.113 359.576 331.402C448.913 285.418 513.454 200.023 381.761 163.819L381.781 163.839Z" fill="#4BCC00"/>
<path d="M266.363 192.26C266.363 219.514 244.427 241.588 217.386 241.588C190.345 241.588 168.409 219.514 168.409 192.26C168.409 165.006 190.345 142.952 217.386 142.952C244.427 142.952 266.363 165.025 266.363 192.26Z" fill="white"/>
<path d="M443.077 263.251C403.408 291.413 358.25 312.771 294.247 312.771C264.289 312.771 258.205 280.705 238.399 296.419C228.17 304.537 192.129 322.687 163.514 321.315C134.65 319.923 88.5711 303.03 75.6168 241.544C70.4927 303.03 67.8826 348.337 44.9488 400.254C101.246 482.993 199.405 524.157 298.239 506.255C287.626 431.587 352.416 358.465 388.919 321.044C402.736 306.876 429.221 283.739 443.077 263.251Z" fill="#4BCC00"/>
<ellipse cx="238.03" cy="191.923" rx="28.7401" ry="40.2361" fill="#0D1217"/>
<path d="M249.526 118.863C265.206 125.131 322.467 144.203 347.242 151.689C321.947 109.717 283.613 112.139 249.526 118.863Z" fill="#35AF00"/>
<path d="M238.03 191.918L203.541 168.926V214.91L238.03 191.918Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_207_2675">
<rect width="506.623" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill-rule="evenodd" viewBox="0 0 252 300" focusable="false" class="chakra-icon custom-1jrmobr" fill="white"><path d="M151.818 106.866c9.177-4.576 20.854-11.312 32.545-20.541 2.465 5.119 2.735 9.586 1.465 13.193-.9 2.542-2.596 4.753-4.826 6.512-2.415 1.901-5.431 3.285-8.765 4.033-6.326 1.425-13.712.593-20.419-3.197m1.591 46.886l12.148 7.017c-24.804 13.902-31.547 39.716-39.557 64.859-8.009-25.143-14.753-50.957-39.556-64.859l12.148-7.017a5.95 5.95 0 003.84-5.845c-1.113-23.547 5.245-33.96 13.821-40.498 3.076-2.342 6.434-3.518 9.747-3.518s6.671 1.176 9.748 3.518c8.576 6.538 14.934 16.951 13.821 40.498a5.95 5.95 0 003.84 5.845zM126 0c14.042.377 28.119 3.103 40.336 8.406 8.46 3.677 16.354 8.534 23.502 14.342 3.228 2.622 5.886 5.155 8.814 8.071 7.897.273 19.438-8.5 24.796-16.709-9.221 30.23-51.299 65.929-80.43 79.589-.012-.005-.02-.012-.029-.018-5.228-3.992-11.108-5.988-16.989-5.988s-11.76 1.996-16.988 5.988c-.009.005-.017.014-.029.018-29.132-13.66-71.209-49.359-80.43-79.589 5.357 8.209 16.898 16.982 24.795 16.709 2.929-2.915 5.587-5.449 8.814-8.071C69.31 16.94 77.204 12.083 85.664 8.406 97.882 3.103 111.959.377 126 0m-25.818 106.866c-9.176-4.576-20.854-11.312-32.544-20.541-2.465 5.119-2.735 9.586-1.466 13.193.901 2.542 2.597 4.753 4.826 6.512 2.416 1.901 5.432 3.285 8.766 4.033 6.326 1.425 13.711.593 20.418-3.197"></path><path d="M197.167 75.016c6.436-6.495 12.107-13.684 16.667-20.099l2.316 4.359c7.456 14.917 11.33 29.774 11.33 46.494l-.016 26.532.14 13.754c.54 33.766 7.846 67.929 24.396 99.193l-34.627-27.922-24.501 39.759-25.74-24.231L126 299.604l-41.132-66.748-25.739 24.231-24.501-39.759L0 245.25c16.55-31.264 23.856-65.427 24.397-99.193l.14-13.754-.016-26.532c0-16.721 3.873-31.578 11.331-46.494l2.315-4.359c4.56 6.415 10.23 13.603 16.667 20.099l-2.01 4.175c-3.905 8.109-5.198 17.176-2.156 25.799 1.961 5.554 5.54 10.317 10.154 13.953 4.48 3.531 9.782 5.911 15.333 7.161 3.616.814 7.3 1.149 10.96 1.035-.854 4.841-1.227 9.862-1.251 14.978L53.2 160.984l25.206 14.129a41.926 41.926 0 015.734 3.869c20.781 18.658 33.275 73.855 41.861 100.816 8.587-26.961 21.08-82.158 41.862-100.816a41.865 41.865 0 015.734-3.869l25.206-14.129-32.665-18.866c-.024-5.116-.397-10.137-1.251-14.978 3.66.114 7.344-.221 10.96-1.035 5.551-1.25 10.854-3.63 15.333-7.161 4.613-3.636 8.193-8.399 10.153-13.953 3.043-8.623 1.749-17.689-2.155-25.799l-2.01-4.175z"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,51 @@
<svg width="128" height="128" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_11565_169621)">
<g filter="url(#filter0_d_11565_169621)">
<path d="M3.09074 25.1666C4.44267 27.0471 6.17683 28.6205 8.1795 29.7838C10.1822 30.947 12.4081 31.6738 14.7114 31.9165C13.5264 30.1333 11.8039 28.4928 9.65354 27.2438C7.50318 25.9948 5.22592 25.3125 3.09074 25.1666Z" fill="url(#paint0_linear_11565_169621)"/>
<path d="M12.543 22.2705C8.40015 19.8636 3.91612 19.2502 0.707663 20.3338C1.0174 21.3575 1.42589 22.3487 1.92738 23.2934C4.71498 23.2288 7.75856 23.9859 10.5906 25.6308C13.4227 27.2757 15.5888 29.5459 16.9143 32C17.9839 31.9672 19.0479 31.8309 20.0913 31.5932C19.4426 28.2698 16.6849 24.6779 12.543 22.2705Z" fill="url(#paint1_linear_11565_169621)"/>
<path d="M32.2852 12.5009C31.7585 10.3584 30.8054 8.34403 29.4829 6.57804C28.1604 4.81205 26.4956 3.33067 24.5879 2.22235C22.6802 1.11403 20.5687 0.401504 18.3796 0.127309C16.1904 -0.146885 13.9684 0.0228794 11.8463 0.626465C15.3915 1.06033 19.3267 2.39122 23.1859 4.63324C27.0452 6.87525 30.1533 9.63411 32.2852 12.5009Z" fill="url(#paint2_linear_11565_169621)"/>
<path d="M27.1271 20.3583C25.3124 17.3446 22.2038 14.4588 18.3743 12.2342C14.5449 10.0095 10.4991 8.7388 6.98531 8.65474C3.894 8.58152 1.57389 9.48017 0.621548 11.1197C0.616125 11.1294 0.608532 11.1386 0.602566 11.1484C0.516877 11.4559 0.44312 11.7639 0.37587 12.0731C1.70568 11.5481 3.24645 11.2558 4.95969 11.2232C8.76959 11.1517 13.0334 12.3703 16.9681 14.6562C20.9027 16.9422 24.0759 20.0438 25.9003 23.3878C26.7182 24.8944 27.2285 26.3777 27.4308 27.7948C27.6662 27.5844 27.8972 27.3669 28.1212 27.1408C28.1272 27.1305 28.131 27.1196 28.1369 27.1088C29.0893 25.4677 28.721 23.0076 27.1271 20.3583Z" fill="url(#paint3_linear_11565_169621)"/>
<path d="M15.4609 17.2485C9.59662 13.8416 3.11626 13.3079 0 15.6855C0.00612096 16.4297 0.0630166 17.1726 0.170292 17.9091C1.08699 17.6312 2.03177 17.4562 2.98718 17.3874C6.46952 17.1254 10.3087 18.0957 13.7927 20.1207C17.2766 22.1458 20.023 25.0018 21.5209 28.1543C21.935 29.018 22.2508 29.9254 22.4624 30.8595C23.1555 30.5878 23.8294 30.2694 24.4794 29.9066C25.0011 26.0213 21.3268 20.656 15.4609 17.2485Z" fill="url(#paint4_linear_11565_169621)"/>
<path d="M30.1434 15.3141C28.3082 12.3036 25.1724 9.40969 21.3158 7.17039C17.4593 4.93109 13.3977 3.64033 9.87257 3.53674C7.1853 3.45919 5.10382 4.11053 4.02457 5.34109C8.50588 4.58182 14.4168 5.85794 20.146 9.18625C25.8753 12.5146 29.9135 17.0181 31.4722 21.2868C32.0064 19.7406 31.5416 17.6098 30.1434 15.3141Z" fill="url(#paint5_linear_11565_169621)"/>
</g>
</g>
<defs>
<filter id="filter0_d_11565_169621" x="-22.7449" y="-20.4704" width="77.7749" height="77.4898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2.27449"/>
<feGaussianBlur stdDeviation="11.3724"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_11565_169621"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_11565_169621" result="shape"/>
</filter>
<linearGradient id="paint0_linear_11565_169621" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#C7F284"/>
<stop offset="1" stop-color="#00BEF0"/>
</linearGradient>
<linearGradient id="paint1_linear_11565_169621" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#C7F284"/>
<stop offset="1" stop-color="#00BEF0"/>
</linearGradient>
<linearGradient id="paint2_linear_11565_169621" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#C7F284"/>
<stop offset="1" stop-color="#00BEF0"/>
</linearGradient>
<linearGradient id="paint3_linear_11565_169621" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#C7F284"/>
<stop offset="1" stop-color="#00BEF0"/>
</linearGradient>
<linearGradient id="paint4_linear_11565_169621" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#C7F284"/>
<stop offset="1" stop-color="#00BEF0"/>
</linearGradient>
<linearGradient id="paint5_linear_11565_169621" x1="21.5" y1="6.5" x2="6.66667" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.0001" stop-color="#C7F284"/>
<stop offset="1" stop-color="#00BEF0"/>
</linearGradient>
<clipPath id="clip0_11565_169621">
<rect width="32.2852" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -31,6 +31,31 @@ body {
font-weight: 400;
font-style: normal;
}
/* Chat notification animation */
@keyframes fade-in-out {
0% {
opacity: 0;
transform: translateX(20px);
}
10% {
opacity: 1;
transform: translateX(0);
}
90% {
opacity: 1;
transform: translateX(0);
}
100% {
opacity: 0;
transform: translateX(20px);
}
}
.animate-fade-in-out {
animation: fade-in-out 3s ease-in-out forwards;
}
/* Grid animation */
@keyframes scrollGrid {
from {

View File

@ -4,7 +4,8 @@ import Footer from "@/components/Footer";
import Header from "@/components/Header";
import HeroSection from "@/components/HeroSection";
import Leaderboard from "@/components/Leaderboard";
// import Leaderboard from "@/components/Leaderboard";
import Activities from "@/components/Activities";
import GlobalChat from "@/components/GlobalChat";
import { PrivyProvider } from "@privy-io/react-auth";
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
import { Toaster } from "sonner";
@ -39,6 +40,11 @@ export default function Home() {
<Header />
<HeroSection />
<Leaderboard />
<div className="container mt-10"></div>
<Activities />
<div className="container mt-10"></div>
<GlobalChat />
<Footer />
</>
</PrivyProvider>

View File

@ -0,0 +1,235 @@
import Image from "next/image";
import { useState, useEffect, useRef } from "react";
import { API_BASE_URL } from "@/data/shared";
import { PlusCircleIcon, XCircleIcon, UserPlusIcon, TrophyIcon } from '@heroicons/react/24/outline';
import { GetGameByID } from "@/data/games";
import { toast } from "sonner";
interface Activity {
id: string;
type: string;
owner_id: string;
joiner_id: string | null;
game: string;
amount: string;
comments: string | null;
time: string;
}
interface UserData {
id: string;
username: string;
bio: string;
x_profile_url: string;
}
export default function Activities() {
const [activities, setActivities] = useState<Activity[]>([]);
const [userData, setUserData] = useState<Record<string, UserData>>({});
const [loading, setLoading] = useState(true);
const [failedImages, setFailedImages] = useState<Set<string>>(new Set());
const defaultPFP = '/duelfiassets/PFP (1).png';
const isFirstLoad = useRef(true);
const previousActivitiesRef = useRef<Activity[]>([]);
const pendingToasts = useRef<Activity[]>([]);
const formatActivityMessage = (activity: Activity) => {
const ownerUsername = userData[activity.owner_id]?.username || "Anonymous";
const joinerUsername = activity.joiner_id ? (userData[activity.joiner_id]?.username || "Anonymous") : null;
let amount = parseFloat(activity.amount);
if(activity.type == "won"){
amount = amount * 1.9;
}
const formattedAmount = amount > 0 ? `${amount} SOL` : "";
const gameData = GetGameByID(activity.game);
const gameName = gameData?.name || activity.game;
switch (activity.type) {
case "create":
return `${ownerUsername} created a ${gameName} game with a ${formattedAmount} entry fee.`;
case "close":
return `${ownerUsername} cancelled their ${gameName} game.`;
case "join":
return `${joinerUsername} joined ${gameName} with a ${formattedAmount} entry fee.`;
case "won":
return `${ownerUsername} won ${formattedAmount} in ${gameName}!`;
default:
return "Unknown activity";
}
};
useEffect(() => {
// Show pending toasts when userData is updated
if (Object.keys(userData).length > 0 && pendingToasts.current.length > 0) {
pendingToasts.current.forEach((activity) => {
const message = formatActivityMessage(activity);
const toastConfig = {
duration: 5000,
position: 'top-right' as const,
icon: activity.type === "create" ? "🎮" :
activity.type === "close" ? "❌" :
activity.type === "join" ? "👋" :
activity.type === "won" ? "🏆" : "📢"
};
toast(message, toastConfig);
});
pendingToasts.current = [];
}
}, [userData]);
useEffect(() => {
const fetchActivities = async () => {
try {
const response = await fetch("/api/v1/get_activities.php");
const data = await response.json();
// Don't show toasts on first load
if (!isFirstLoad.current) {
// Check for new activities using the ref
const newActivities = data.filter((activity: Activity) =>
!previousActivitiesRef.current.some(prevActivity => prevActivity.id === activity.id)
);
if (newActivities.length > 0) {
// Store new activities in pendingToasts
pendingToasts.current = newActivities;
}
} else {
isFirstLoad.current = false;
}
// Update the ref with current activities
previousActivitiesRef.current = data;
setActivities(data);
// Fetch user data for all unique user IDs
const userIds = new Set<string>();
data.forEach((activity: Activity) => {
if (activity.owner_id) userIds.add(activity.owner_id);
if (activity.joiner_id) userIds.add(activity.joiner_id);
});
const userDataPromises = Array.from(userIds).map(async (userId) => {
try {
const response = await fetch(`/api/v1/get_user_by_id.php?id=${userId}`);
const data = await response.json();
return [userId, data];
} catch (error) {
console.error(`Error fetching user data for ${userId}:`, error);
return [userId, null];
}
});
const userDataResults = await Promise.all(userDataPromises);
const userDataMap = Object.fromEntries(userDataResults);
setUserData(userDataMap);
} catch (error) {
console.error("Error fetching activities:", error);
} finally {
setLoading(false);
}
};
// Initial fetch
fetchActivities();
// Set up interval for periodic updates
const intervalId = setInterval(fetchActivities, 5000);
// Cleanup function to clear the interval when component unmounts
return () => clearInterval(intervalId);
}, []); // Remove dependencies since we're using refs
const formatTimeAgo = (timestamp: string) => {
const date = new Date(timestamp + 'Z');
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const diffInMinutes = Math.floor(diffInSeconds / 60);
const diffInHours = Math.floor(diffInMinutes / 60);
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays > 0) {
return date.toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true
});
}
if (diffInHours > 0) {
return `${diffInHours} hour${diffInHours === 1 ? '' : 's'} ago`;
}
if (diffInMinutes > 0) {
return `${diffInMinutes} minute${diffInMinutes === 1 ? '' : 's'} ago`;
}
return "less than 1 minute ago";
};
if (loading) {
return (
<div className="container mx-auto max-w-screen-xl px-3">
<div className="bg-[rgb(30,30,30)] rounded-xl p-6 shadow-lg">
<h2 className="text-2xl font-bold text-white mb-4">Recent Activities</h2>
<div className="flex justify-center items-center h-40">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[rgb(248,144,22)]"></div>
</div>
</div>
</div>
);
}
return (
<div className="container mx-auto max-w-screen-xl px-3 md:px-40">
<div className="bg-[rgb(30,30,30)] rounded-xl p-6 shadow-lg">
<h2 className="text-2xl font-bold text-white mb-4">Recent Activities</h2>
<div className="space-y-4">
{[...activities].sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()).map((activity) => {
const userId = activity.type === "join" ? activity.joiner_id : activity.owner_id;
if (!userId) return null;
let profileUrl = userData[userId]?.x_profile_url || `${API_BASE_URL}profile_pics/${userId}.jpg`;
if (failedImages.has(profileUrl)) {
profileUrl = defaultPFP;
}
return (
<div
key={activity.id}
className="flex items-center gap-4 p-3 px-6 rounded-lg bg-[rgb(10,10,10)] border border-[rgb(30,30,30)] hover:border-[rgb(248,144,22)] transition-all duration-300"
>
<Image
src={profileUrl}
alt="Profile"
width={40}
height={40}
className="rounded-full border border-gray-700 object-cover"
onError={(e) => {
// @ts-expect-error - Type mismatch expected
e.target.src = defaultPFP;
setFailedImages(prev => new Set(prev).add(profileUrl));
}}
/>
<div className="flex-1">
<p className="text-white">{formatActivityMessage(activity)}</p>
<p className="text-sm text-gray-400">
{formatTimeAgo(activity.time)}
</p>
</div>
<div>
{activity.type === "create" && <PlusCircleIcon className="w-6 h-6 text-green-500" />}
{activity.type === "close" && <XCircleIcon className="w-6 h-6 text-red-500" />}
{activity.type === "join" && <UserPlusIcon className="w-6 h-6 text-[rgb(248,144,22)]" />}
{activity.type === "won" && <TrophyIcon className="w-6 h-6 text-[rgb(248,144,22)]" />}
</div>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@ -37,7 +37,7 @@ export function FirstVisitModal({ isOpen, onClose }: FirstVisitModalProps) {
<div className="space-y-4">
<p className="text-gray-300">
Challenge your friends (or strangers) in a head-to-head skill games and earn from your victories!
Challenge your friends (or strangers) in head-to-head skill games and earn from your victories!
<br />
<br />
Create or join duels, set an entry fee, and the winner takes all.

View File

@ -4,6 +4,13 @@ import { URL_TELEGRAM, URL_TWITTER } from "@/shared/constants";
import Image from "next/image";
import Link from "next/link";
const TRACK_TRADE_LINKS = {
BELIEVE: "https://believe.app/coin/FiQBQdASK3AJVZfE7Nc5KwHvQiTd8YepsHAexopPF1eS",
COINGECKO: "https://www.coingecko.com/en/search_redirect?id=duel-fi&type=coin",
DEXSCREENER: "https://dexscreener.com/solana/2HuT5v8fG2c5VHxoq1thaN4c2ARwUunEnhbibyQJC1qX",
METEORA: "https://www.meteora.ag/dlmm/8CXwRcwLzwHgj8F6nXbeYa2CgEGTAtGsFHmtjJ9dhGQ9"
};
export default function Footer() {
return (
<footer className="w-full bg-[rgb(17,17,17)] text-white py-8">
@ -50,6 +57,64 @@ export default function Footer() {
</a>
</div>
{/* Track & Trade Section */}
<div className="mt-8">
<div className="flex gap-6 items-center">
<a
href={TRACK_TRADE_LINKS.BELIEVE}
target="_blank"
rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-110"
>
<Image
src="/duelfiassets/believe.svg"
alt="Believe"
width={32}
height={32}
/>
</a>
<a
href={TRACK_TRADE_LINKS.COINGECKO}
target="_blank"
rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-110"
>
<Image
src="/duelfiassets/coingecko.svg"
alt="CoinGecko"
width={32}
height={32}
/>
</a>
<a
href={TRACK_TRADE_LINKS.DEXSCREENER}
target="_blank"
rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-110"
>
<Image
src="/duelfiassets/dexscreener.svg"
alt="DexScreener"
width={32}
height={32}
/>
</a>
<a
href={TRACK_TRADE_LINKS.METEORA}
target="_blank"
rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-110"
>
<Image
src="/duelfiassets/meteora.svg"
alt="Meteora"
width={32}
height={32}
/>
</a>
</div>
</div>
{/* Spacer */}
<div className="h-6"></div>

View File

@ -10,7 +10,6 @@ import { createBet } from "@/shared/solana_helpers";
import { Game } from "@/types/Game";
import { connection, EXPLORER_TX_TEMPLATE } from "@/data/shared";
import { CONFIRMATION_THRESHOLD } from "@/shared/constants";
import { fetchUserById, showNewGameNotification } from "@/shared/data_fetcher";
interface GameModalProps {
isOpen: boolean;
onClose: () => void;
@ -64,7 +63,7 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) {
setIsProcessing(true);
toast.loading("Creating Game");
try {
const ownerProfile = await fetchUserById(user?.id ?? "");
//const ownerProfile = await fetchUserById(user?.id ?? "");
const tx = await createBet(wallet, user?.id ?? "", selectedPrice, selectedGame.id, false);
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
if (tx.length > 5) {
@ -78,7 +77,7 @@ export default function GameModal({ isOpen, onClose }: GameModalProps) {
},
});
showNewGameNotification(ownerProfile?.username ?? "", selectedGame.name, selectedPrice.toString());
//showNewGameNotification(ownerProfile?.username ?? "", selectedGame.name, selectedPrice.toString());
})

View File

@ -0,0 +1,219 @@
"use client";
import { useEffect, useState, useRef } from 'react';
import { usePrivy } from '@privy-io/react-auth';
import { io, Socket } from 'socket.io-client';
import { fetchUserById } from '@/shared/data_fetcher';
import { ChatBubbleLeftIcon, XMarkIcon } from '@heroicons/react/24/outline';
interface ChatMessage {
id: string;
user: string;
message: string;
timestamp: number;
}
interface UserData {
id: string;
username: string;
bio: string;
x_profile_url: string;
}
export default function GlobalChat() {
const { user, authenticated } = usePrivy();
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [newMessage, setNewMessage] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [userData, setUserData] = useState<Record<string, UserData>>({});
const [isCollapsed, setIsCollapsed] = useState(true);
const [notification, setNotification] = useState<{ message: string; username: string } | null>(null);
const notificationTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (!authenticated) return;
// Initialize socket connection
const socketInstance = io('https://wschat.duelfi.io', {
auth: {
token: user?.id // Using Privy user ID as authentication
}
});
socketInstance.on('connect', () => {
setIsConnected(true);
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
});
socketInstance.on('chat message', async (message: ChatMessage) => {
setMessages(prev => [...prev, message]);
// Fetch user data if we haven't already
if (!userData[message.user]) {
const userInfo = await fetchUserById(message.user);
if (userInfo) {
setUserData(prev => ({
...prev,
[message.user]: userInfo
}));
// Show notification if chat is collapsed
if (isCollapsed && message.user !== user?.id) {
showNotification(message.message, userInfo.username);
}
}
} else if (isCollapsed && message.user !== user?.id) {
// Show notification if chat is collapsed and we already have the user data
showNotification(message.message, userData[message.user].username);
}
});
socketInstance.on('recent messages', (messages: ChatMessage[]) => {
setMessages(messages);
messages.forEach(message => {
if (!userData[message.user]) {
fetchUserById(message.user).then(userInfo => {
setUserData(prev => ({ ...prev, [message.user]: userInfo }));
});
}
});
});
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
if (notificationTimeoutRef.current) {
clearTimeout(notificationTimeoutRef.current);
}
};
}, [authenticated, user, isCollapsed]);
const showNotification = (message: string, username: string) => {
// Clear any existing notification timeout
if (notificationTimeoutRef.current) {
clearTimeout(notificationTimeoutRef.current);
}
setNotification({ message, username });
// Set new timeout to clear notification
notificationTimeoutRef.current = setTimeout(() => {
setNotification(null);
}, 3000);
};
const sendMessage = (e: React.FormEvent) => {
e.preventDefault();
if (!socket || !newMessage.trim() || !user) return;
const message: ChatMessage = {
id: Date.now().toString(),
user: user.id,
message: newMessage.trim(),
timestamp: Date.now()
};
socket.emit('chat message', message);
setNewMessage('');
};
if (!authenticated) {
return (
<div className="p-4 text-center text-gray-400">
Please log in to join the chat
</div>
);
}
if (isCollapsed) {
return (
<div className="fixed bottom-4 right-4 flex items-center gap-2">
{notification && (
<div className="bg-[rgb(40,40,40)] text-white px-4 py-2 rounded-lg shadow-lg animate-fade-in-out">
<div className="text-sm font-semibold">{notification.username}</div>
<div className="text-sm text-gray-300">{notification.message}</div>
</div>
)}
<button
onClick={() => setIsCollapsed(false)}
className="bg-[rgb(248,144,22)] text-black p-4 rounded-full shadow-lg hover:scale-110 transition-all duration-300"
>
<ChatBubbleLeftIcon className="w-6 h-6" />
</button>
</div>
);
}
return (
<div className="fixed bottom-4 right-4 w-80 bg-[rgb(30,30,30)] rounded-lg shadow-lg">
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold text-white">Global Chat</h3>
<div className="text-sm text-gray-400">
{isConnected ? 'Connected' : 'Disconnected'}
</div>
</div>
<button
onClick={() => setIsCollapsed(true)}
className="text-gray-400 hover:text-white transition-colors"
>
<XMarkIcon className="w-6 h-6" />
</button>
</div>
<div className="h-96 overflow-y-auto p-4 space-y-2">
{messages.map((msg) => (
<div
key={msg.id}
className={`p-2 rounded ${
msg.user === user?.id
? 'bg-[rgb(100,60,10)] bg-opacity-20'
: 'bg-[rgb(40,40,40)]'
}`}
>
<div className="text-sm text-gray-400 flex justify-between items-center">
<span>
{msg.user === user?.id ? 'You' : userData[msg.user]?.username || `User ${msg.user.slice(0, 6)}`}
</span>
<span className="text-xs">
{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className="text-white">{msg.message}</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="p-4 border-t border-gray-700">
<div className="flex gap-2">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-[rgb(40,40,40)] text-white px-3 py-2 rounded focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
<button
type="submit"
className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600 transition-colors"
>
Send
</button>
</div>
</form>
</div>
);
}

View File

@ -36,10 +36,10 @@ export function HowItWorksModal({ isOpen, onClose }: HowItWorksModalProps) {
<div className="space-y-4">
{[
{ step: "Sign In or Connect Your Wallet", desc: "Sign up with X, Google, or connect an existing wallet—your wallet is created instantly and securely with Privy." },
{ step: "Create or Join a Game", desc: "Start your own match with your preferred entry amount, or join an existing one." },
{ step: "Place Your Entry", desc: "Confirm it and get ready to play." },
{ step: "Win & Get Paid", desc: "Win the game and your prize is automatically sent to your wallet." },
{ step: "Sign In or Connect Your Wallet", desc: "Log in with X, Google, or connect your wallet. A secure wallet is created instantly with Privy." },
{ step: "Create or Join a Game", desc: "Start a game with your own entry amount or join one that's already waiting." },
{ step: "Enter the Game", desc: "Confirm your entry and stay on the website—the game will start automatically once all players are ready." },
{ step: "Win and Get Paid", desc: "Win the match and your prize goes straight to your wallet." },
].map(({ step, desc }, index) => (
<div key={index}>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">

View File

@ -6,7 +6,7 @@ interface PriceSelectionProps {
}
export function PriceSelection({ selectedPrice, onSelect }: PriceSelectionProps) {
const presets = [0.05, 0.1, 0.2, 0.5, 1.0];
const presets = [0.01,0.05, 0.1, 0.2, 0.5, 1.0];
const [inputValue, setInputValue] = useState<string>("");
const MIN_AMOUNT = 0.01;

View File

@ -215,13 +215,26 @@ export default function PrivyButton() {
};
useEffect(() => {
const intervalId = setInterval(() => {
if (wallets) {
fetchSolBalance();
}, 15000); // 5000 milliseconds = 5 seconds
}
}, [user, wallets]);
useEffect(() => {
const intervalId = setInterval(() => {
if (wallets && solWallet) {
fetchSolBalance();
}
}, 15000); // 15000 milliseconds = 15 seconds
// Cleanup function to clear the interval on unmount or when `ready` changes
// Initial fetch
if (wallets && solWallet) {
fetchSolBalance();
}
// Cleanup function to clear the interval on unmount or when dependencies change
return () => clearInterval(intervalId);
}, [ready]);
}, [wallets, solWallet]);
const saveProfileChanges = async () => {
if (!user) {
@ -273,12 +286,6 @@ export default function PrivyButton() {
}
};
useEffect(() => {
if (wallets) {
fetchSolBalance();
}
}, [user, wallets]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {

View File

@ -11,8 +11,21 @@ export async function fetchUserById(id: string) {
export async function showNewGameNotification(username:string, game:string, wager:string){
try{
const isDevnet = CLUSTER_URL === clusterApiUrl("devnet");
await fetch(`${API_URL}send_telegram_notification.php?username=${username}&wager=${wager}&game=${game}&devnet=${isDevnet ? "1" : "0"}`)
const url = `${API_URL}send_telegram_notification.php?username=${username}&wager=${wager}&game=${game}&devnet=${isDevnet ? "1" : "0"}`;
console.log(url);
await fetch(url);
}catch(error){
console.error("Error showing new game notification:", error);
}
}
export async function add_new_activity(type:string, owner_id:string, joiner_id:string, game:string, amount:number ){
try{
const isDevnet = CLUSTER_URL === clusterApiUrl("devnet");
const url = `${API_URL}add_activity.php?type=${type}&owner_id=${owner_id}&joiner_id=${joiner_id}&game=${game}&amount=${amount}&devnet=${isDevnet ? "1" : "0"}`;
console.log(url);
await fetch(url);
}catch(error){
console.error("Error adding new activity:", error);
}
}

View File

@ -7,7 +7,7 @@ import idl from "../idl/bets_idl.json";
import { Bet } from "@/types/Bet";
import { toast } from "sonner";
import { CONFIRMATION_THRESHOLD } from "./constants";
import { add_new_activity } from "./data_fetcher";
export async function fetchOpenBets(): Promise<Bet[]> {
const response = await fetch(`${VALIDATOR_URL}fetchBets`);
@ -78,11 +78,12 @@ export async function closeBet(wallets: ConnectedSolanaWallet, uid:string, betI
const betList = await program.account.betsList.fetch(bet_list_pda);
let chosenBet: PublicKey | null = null;
let game_id:string = "";
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;
game_id = betAcc.gameId.toString();
break;
}
}
@ -93,7 +94,7 @@ export async function closeBet(wallets: ConnectedSolanaWallet, uid:string, betI
}
const winner = new PublicKey(wallets.address);
const chosenBetVaultAcc = await program.account.betVault.fetch(chosenBet);
// Execute the closeBet transaction
const tx = await program.methods
@ -112,8 +113,8 @@ export async function closeBet(wallets: ConnectedSolanaWallet, uid:string, betI
// Send transaction// Replace with correct RPC endpoint
const txId = await connection.sendRawTransaction(signedTx.serialize());
console.log(`Transaction: ${tx}`);
add_new_activity("close", uid, chosenBetVaultAcc.joinerId, game_id, chosenBetVaultAcc.wager.toNumber() / LAMPORTS_PER_SOL);
console.log(`Transaction: ${txId}`);
return txId;
} catch (error) {
console.error("Error closing bet:", error);
@ -161,7 +162,7 @@ export async function createBet(wallets: ConnectedSolanaWallet, uid: string, sel
const txId = await connection.sendRawTransaction(signedTx.serialize());
console.log(`Transaction sent: ${txId}`);
console.log(`Transaction confirmed: ${txId}`);
add_new_activity("create", uid, "", selectedGame, selectedPrice);
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()
@ -231,10 +232,10 @@ export async function joinBet(wallets: ConnectedSolanaWallet, uid: string, gameI
// Sign transaction with Privy
const signedTx = await wallet.signTransaction(tx);
const chosenBetVaultAcc = await program.account.betVault.fetch(betVaultPubkey);
// Send transaction// Replace with correct RPC endpoint
const txId = await connection.sendRawTransaction(signedTx.serialize());
add_new_activity("join", "",uid, gameId, chosenBetVaultAcc.wager.toNumber() / LAMPORTS_PER_SOL);
console.log(`Transaction ID: ${txId}`);
return txId;
} catch (error: unknown) {