This commit is contained in:
Sewmina 2025-03-30 00:59:33 +05:30
parent ae64342e76
commit 0a8d247391
10 changed files with 5072 additions and 43 deletions

4637
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,15 +10,22 @@
},
"dependencies": {
"@fontsource/manrope": "^5.2.5",
"@privy-io/react-auth": "^2.7.2",
"@solana/web3.js": "^1.98.0",
"bs58": "^6.0.0",
"ethers": "^6.13.5",
"next": "15.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0"
"react": "^19.1.0",
"react-auth": "^1.0.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-toastify": "^11.0.5",
"viem": "^2.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",

View File

@ -31,3 +31,24 @@ body {
font-weight: 400;
font-style: normal;
}
/* Grid animation */
@keyframes scrollGrid {
from {
transform: perspective(1000px) rotateX(45deg) translateY(0);
}
to {
transform: perspective(1000px) rotateX(45deg) translateY(-100px);
}
}
/* Grid container */
.grid-container {
position: absolute;
width: 100vw;
height: 100vh;
background-image:
linear-gradient(rgba(100, 100, 100, 0.3) 1px, transparent 1px),
linear-gradient(to right, rgba(100, 100, 100, 0.3) 1px, transparent 1px);
background-size: 50px 50px;
animation: scrollGrid 2s linear infinite;
}

View File

@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { PrivyProvider } from "@privy-io/react-auth";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
@ -23,6 +23,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}

View File

@ -1,12 +1,16 @@
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import HeroSection from "@/components/Home";
import Image from "next/image";
export default function Home() {
return (
<div className="bg-[rgb(22,22,22)]">
<Header/>
<HeroSection/>
<Footer/>
</div>
);
}

11
src/components/3DGrid.tsx Normal file
View File

@ -0,0 +1,11 @@
"use client";
export default function ThreeDGrid() {
return (
<div className="relative w-full h-[100px] overflow-hidden bg-black">
<div className="absolute inset-0 perspective-1000">
<div className="grid-container animate-scrollGrid" />
</div>
</div>
);
}

View File

@ -4,16 +4,39 @@ import Link from "next/link";
import Image from "next/image";
import { FaTelegram, FaXTwitter } from "react-icons/fa6";
import { useState } from "react";
import { PrivyProvider, usePrivy } from "@privy-io/react-auth";
import PrivyButton from "./PrivyButton";
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
export default function Header() {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const { login, logout, user } = usePrivy();
// Toggle the nav drawer
const toggleDrawer = () => {
setIsDrawerOpen(!isDrawerOpen);
};
return (
<><PrivyProvider
appId="cm8spd7l600lfe4am1phq9qq8"
clientId="client-WY5i4HS6T7JP44iKMQUyZXwftzwKLFvEsGvMtFY1znXSj"
config={{
// Customize Privy's appearance in your app
appearance: {
theme: 'dark',
accentColor: '#f89016',
logo: 'https://your-logo-url'
},
// Create embedded wallets for users who don't have a wallet
embeddedWallets: {
createOnLogin: 'users-without-wallets'
},
externalWallets:{
solana:{connectors:toSolanaWalletConnectors()}
}
}}
>
<header className="w-full bg-[rgb(22,22,22)] shadow-md">
<div className="container mx-auto flex items-center justify-between p-12 max-w-screen-xl w-full">
{/* Logo */}
@ -69,10 +92,11 @@ export default function Header() {
/>
</a>
{/* Login Button */}
{/* Login Button
<button className="px-15 py-3 bg-[rgb(248,144,22)] text-black font-bold rounded-lg transition-transform duration-300 hover:scale-115 hover:bg-white hover:text-[rgb(248,144,22)]">
Connected
</button>
</button>*/}
<PrivyButton></PrivyButton>
</div>
{/* Mobile-only view (Logo & Login) */}
@ -171,5 +195,6 @@ export default function Header() {
</div>
)}
</header>
</PrivyProvider></>
);
}

122
src/components/Home.tsx Normal file
View File

@ -0,0 +1,122 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import OpenGames from "./OpenGames";
export default function HeroSection() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<section className="flex flex-col items-center text-center py-16">
{/* Centered Image */}
<Image
src="/duelfiassets/Playing on Arcade Machine no BG.png"
alt="DuelFi Hero"
width={150}
height={50}
className="mb-6"
/>
{/* Big Text */}
<h1 className="text-4xl font-bold text-white">
Instant <span className="text-[rgb(248,144,22)]">Duels</span>, Instant{" "}
<span className="text-[rgb(248,144,22)]">Wins</span>
</h1>
{/* Create a Game Button */}
<button className="mt-12 px-12 py-4 bg-[rgb(248,144,22)] hover:bg-white text-black text-sm font-semibold rounded-xl flex items-center gap-2 transition duration-300 hover:scale-110">
Create a Game
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M7 7h10v10" />
<path d="M7 17 17 7" />
</svg>
</button>
{/* How it Works */}
<p
className="mt-4 text-gray-400 cursor-pointer hover:underline font-mono"
onClick={() => setIsModalOpen(true)}
>
How it works?..
</p>
<OpenGames />
</section>
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50">
<div
className="bg-[rgb(30,30,30)] text-white w-full max-w-lg p-6 rounded-2xl shadow-lg transform transition-transform duration-300 animate-slide-down"
>
<h2 className="text-xl font-bold mb-4">How It Works</h2>
{/* Steps */}
<div className="space-y-4">
<div>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">1. Connect Your Wallet</h3>
<p className="text-xs text-gray-400 font-mono">
Start by linking your wallet securely.
</p>
</div>
<div>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">2. Create or Join Game</h3>
<p className="text-xs text-gray-400 font-mono">
Pick a game and set a wager, or join an existing match.
</p>
</div>
<div>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">3. Place Your Bet</h3>
<p className="text-xs text-gray-400 font-mono">
Confirm your wager and get ready to play.
</p>
</div>
<div>
<h3 className="text-[rgb(248,144,22)] font-bold text-lg">4. Claim Your Winnings</h3>
<p className="text-xs text-gray-400 font-mono">
Win the game and collect your rewards instantly!
</p>
</div>
</div>
{/* Okay Button */}
<button
className="mt-6 w-full bg-[rgb(248,144,22)] text-black font-semibold py-2 rounded-xl transition hover:bg-white hover:scale-105"
onClick={() => setIsModalOpen(false)}
>
Okay
</button>
</div>
</div>
)}
{/* Animation */}
<style jsx>{`
@keyframes slide-down {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-slide-down {
animation: slide-down 0.3s ease-out;
}
`}</style>
</>
);
}

View File

@ -0,0 +1,63 @@
"use client";
import Image from "next/image";
const games = [
{
id: 1,
name: "Block Drop",
entryFee: "0.1 SOL",
thumbnail: "/duelfiassets/Block Drop Illustration.jpeg",
},
{
id: 2,
name: "Venom Trails",
entryFee: "0.02 ETH",
thumbnail: "/duelfiassets/Venom Trail Game Cover Illustration.png",
},
{
id: 3,
name: "Wall Smash",
entryFee: "0.05 ETH",
thumbnail: "/duelfiassets/Wall Smash Game Cover Illustration.png",
},
];
export default function OpenGames() {
return (
<section className="py-16 px-6">
<h2 className="text-3xl font-bold text-white mb-6">Open Games</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{games.map((game) => (
<div
key={game.id}
className="relative group bg-[rgb(30,30,30)] rounded-2xl p-2 w-50 overflow-hidden cursor-pointer transition-all duration-300 hover:-translate-y-2"
>
{/* Game Thumbnail */}
<div className="relative w-full h-40 overflow-hidden rounded-xl">
<Image
src={game.thumbnail}
alt={game.name}
layout="fill"
objectFit="cover"
className="transition-transform duration-300 group-hover:scale-110"
/>
{/* Join Button - Centered inside Image */}
<button className="absolute inset-0 flex items-center justify-center bg-black/60 text-white font-semibold rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100">
Join Game
</button>
</div>
{/* Game Info - Left Aligned */}
<div className="mt-4 px-3 text-left">
<h3 className="text-lg font-semibold text-white py-2">{game.name}</h3>
<p className="text-xs text-gray-400 font-mono py-1">Entry Fee</p>
<p className="text-xs text-white font-mono py-1">{game.entryFee}</p>
</div>
</div>
))}
</div>
</section>
);
}

View File

@ -0,0 +1,208 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { usePrivy, useSolanaWallets, useWallets } from "@privy-io/react-auth";
import { Connection, PublicKey } from "@solana/web3.js"; // Solana Web3.js imports
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import bs58 from "bs58"; // Import bs58 library to validate the address format
export default function PrivyButton() {
const { login, logout, user } = usePrivy();
const { wallets } = useSolanaWallets();
const [isModalOpen, setIsModalOpen] = useState(false);
const [solWallet, setSolWallet] = useState("");
const [solBalance, setSolBalance] = useState("--"); // Placeholder
const [tokenBalance, setTokenBalance] = useState("--"); // Placeholder
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const fetchSolBalance = async () => {
wallets.forEach((wallet)=>{
console.log(wallet.address + " : " + wallet.type);
if(wallet.type == "solana"){
setSolWallet(wallet.address);
}
})
if (solWallet!="") {
const walletAddress = solWallet; // Access the Solana wallet address
try {
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const publicKey = new PublicKey(walletAddress);
const balance = await connection.getBalance(publicKey);
setSolBalance((balance / 1e9).toFixed(2)); // Convert lamports to SOL (1 SOL = 1e9 lamports)
} catch (error) {
console.error("Failed to get balance:", error);
setSolBalance("Error");
}
}
};
if (wallets) {
fetchSolBalance();
}
}, [user, wallets]); // Added wallets to dependency array
// Function to validate if the address is a valid Solana address (base58)
const isValidSolanaAddress = (address: string) => {
try {
bs58.decode(address); // Attempt to decode the address
return true; // If no error, it's a valid base58 Solana address
} catch (e) {
return false; // Invalid address format
}
};
// Close modal when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setIsModalOpen(false);
}
}
if (isModalOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isModalOpen]);
const customLogin = () => {
login({ walletChainType: "solana-only" });
};
const copyToClipboard = async () => {
const walletAddress = solWallet ?? ""; // Use an empty string if wallet address is undefined or null
if (walletAddress) {
await navigator.clipboard.writeText(walletAddress);
toast.success("Wallet address copied!"); // Toast notification for successful copy
}
};
return (
<>
{user ? (
<button
onClick={() => setIsModalOpen(true)}
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>
) : (
<button
onClick={customLogin}
className="bg-[rgb(248,144,22)] hover:bg-[rgb(248,200,100)] text-white px-6 py-3 rounded-md transition duration-300 hover:scale-105"
>
Sign In
</button>
)}
{/* Modal */}
{isModalOpen && user ? (
<div className="fixed inset-0 bg-black/70 flex justify-center items-start pt-10 z-50">
<div
ref={modalRef}
className="bg-[rgb(30,30,30)] text-white w-full max-w-md p-6 rounded-2xl shadow-lg transform transition-transform duration-300 animate-slide-down"
>
<h2 className="text-xl font-bold mb-4">Account Info</h2>
{/* Wallet Address with Copy Button */}
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">Connected Wallet</p>
<div className="flex items-center justify-between bg-gray-800 p-2 rounded-md">
<p className="font-mono text-sm truncate">{solWallet}</p>
<button
onClick={copyToClipboard}
className="text-gray-300 hover:text-white transition p-1"
>
📋
</button>
</div>
</div>
{/* SOL Balance */}
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">SOL Balance</p>
<p className="text-sm">{solBalance} SOL</p>
</div>
{/* Token Balance */}
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">Token Balance</p>
<p className="text-sm">{tokenBalance}</p>
</div>
{/* Socials */}
{(user?.discord?.username || user?.email?.address) && (
<div className="mb-4">
<p className="text-gray-400 text-xs font-mono">Socials</p>
<div className="flex gap-2">
{user?.discord?.username && (
<a
href={`https://discord.com/users/${user?.discord?.username}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 text-sm hover:underline"
>
Discord
</a>
)}
{user?.email?.address && (
<span className="text-gray-400 text-sm">{user?.email?.address}</span>
)}
</div>
</div>
)}
{/* Sign Out Button */}
<button
className="mt-6 w-full bg-[rgb(248,144,22)] text-black font-semibold py-2 rounded-xl transition hover:bg-white"
onClick={() => {
logout();
setIsModalOpen(false);
}}
>
Sign Out
</button>
</div>
</div>
) : null}
{/* Toast container */}
<ToastContainer
position="top-right"
autoClose={3000}
hideProgressBar
newestOnTop
closeButton={false}
pauseOnHover
draggable
pauseOnFocusLoss
/>
{/* Animation */}
<style jsx>{`
@keyframes slide-down {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.animate-slide-down {
animation: slide-down 0.3s ease-out;
}
`}</style>
</>
);
}