polished a bit

This commit is contained in:
Sewmina 2025-04-09 17:46:57 +05:30
parent 147ef9e669
commit 6a4de52d06
10 changed files with 212 additions and 67 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -154,3 +154,15 @@ body {
opacity: 1; opacity: 1;
transform: translateX(0); /* Slide the button to the left */ transform: translateX(0); /* Slide the button to the left */
} }
/* Remove spinner arrows in number inputs for WebKit (Chrome, Safari) */
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Remove spinner arrows for Firefox */
input[type='number'] {
-moz-appearance: textfield;
}

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { URL_TELEGRAM, URL_TWITTER } from "@/shared/constants";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@ -22,7 +23,7 @@ export default function Footer() {
{/* Social Buttons */} {/* Social Buttons */}
<div className="flex gap-4"> <div className="flex gap-4">
<a <a
href="https://t.me/yourchannel" href={URL_TELEGRAM}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-110" className="transition-transform duration-300 hover:scale-110"
@ -35,7 +36,7 @@ export default function Footer() {
/> />
</a> </a>
<a <a
href="https://twitter.com/yourhandle" href={URL_TWITTER}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-110" className="transition-transform duration-300 hover:scale-110"

View File

@ -14,9 +14,13 @@ export function GameSelection({ games, selectedGame, onSelect }: GameSelectionPr
<div <div
key={game.id} key={game.id}
className={`p-4 cursor-pointer rounded-xl border-2 transition-transform duration-300 className={`p-4 cursor-pointer rounded-xl border-2 transition-transform duration-300
${selectedGame?.id === game.id ? "scale-110 border-gray-400 bg-gray-800" : "border-gray-600 bg-[rgb(10,10,10)]"} ${selectedGame?.id === game.id
hover:scale-105 hover:border-gray-400`} ? "scale-110 border-gray-400 bg-gray-800"
onClick={() => onSelect(game)} : game.isAvailable
? "border-gray-600 bg-[rgb(10,10,10)] hover:scale-105 hover:border-gray-400"
: "border-gray-800 bg-[rgb(5,5,5)] opacity-50 cursor-not-allowed"
}`}
onClick={() => game.isAvailable && onSelect(game)}
> >
<Image <Image
src={game.thumbnail} src={game.thumbnail}
@ -26,6 +30,9 @@ export function GameSelection({ games, selectedGame, onSelect }: GameSelectionPr
className="mb-2 rounded-md" className="mb-2 rounded-md"
/> />
<h3 className="text-center text-sm">{game.name}</h3> <h3 className="text-center text-sm">{game.name}</h3>
{!game.isAvailable && (
<p className="text-center text-xs text-gray-500 mt-1">Coming Soon</p>
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -5,6 +5,7 @@ import Image from "next/image";
import { FaTelegram, FaXTwitter } from "react-icons/fa6"; import { FaTelegram, FaXTwitter } from "react-icons/fa6";
import { useState } from "react"; import { useState } from "react";
import PrivyButton from "./PrivyButton"; import PrivyButton from "./PrivyButton";
import { URL_TELEGRAM, URL_TWITTER, URL_WHITEPAPER } from "@/shared/constants";
export default function Header() { export default function Header() {
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
@ -25,7 +26,7 @@ export default function Header() {
{/* Navigation Links (hidden on mobile, visible on md+) */} {/* Navigation Links (hidden on mobile, visible on md+) */}
<nav className="hidden md:flex gap-6 ml-6"> <nav className="hidden md:flex gap-6 ml-6">
<Link <Link
href="/whitepaper" href={URL_WHITEPAPER}
className="text-white transition-colors duration-[500ms] hover:text-[rgb(248,144,22)]" className="text-white transition-colors duration-[500ms] hover:text-[rgb(248,144,22)]"
> >
Whitepaper Whitepaper
@ -44,7 +45,7 @@ export default function Header() {
{/* Socials & Login (hidden on mobile, visible on md+) */} {/* Socials & Login (hidden on mobile, visible on md+) */}
<div className="hidden md:flex items-center gap-6"> <div className="hidden md:flex items-center gap-6">
<a <a
href="https://t.me/yourchannel" href={URL_TELEGRAM}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-120" className="transition-transform duration-300 hover:scale-120"
@ -57,7 +58,7 @@ export default function Header() {
/> />
</a> </a>
<a <a
href="https://twitter.com/yourhandle" href={URL_TWITTER}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="transition-transform duration-300 hover:scale-120" className="transition-transform duration-300 hover:scale-120"

View File

@ -19,6 +19,7 @@ export default function YourGames({ bets }: GameModalProps) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const { user } = usePrivy(); const { user } = usePrivy();
const [selectedBet, setSelectedBet] = useState<Bet | null>(null); // Track selected bet const [selectedBet, setSelectedBet] = useState<Bet | null>(null); // Track selected bet
const [isProcessing, setIsProcessing] = useState(false); // Track processing state
const handleJoinGame = async () => { const handleJoinGame = async () => {
if (!selectedBet) return; if (!selectedBet) return;
@ -29,14 +30,16 @@ export default function YourGames({ bets }: GameModalProps) {
wallet = _wallet; wallet = _wallet;
} }
}); });
setIsProcessing(true);
toast.loading("Joining Bet"); toast.loading("Joining Bet");
try {
const tx = await joinBet(wallet, user?.id ?? "", selectedBet.id, selectedBet.address); const tx = await joinBet(wallet, user?.id ?? "", selectedBet.id, selectedBet.address);
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx); const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
if (tx.length > 5) { if (tx.length > 5) {
connection.confirmTransaction(tx, CONFIRMATION_THRESHOLD).finally(()=>{ connection.confirmTransaction(tx, CONFIRMATION_THRESHOLD).finally(()=>{
toast.dismiss(); toast.dismiss();
toast.success("Joined game successfully!", { toast.success("Joined game successfully!", {
action: { action: {
label: "View TX", label: "View TX",
@ -44,12 +47,18 @@ export default function YourGames({ bets }: GameModalProps) {
}, },
}); });
}) })
} else { } else {
toast.dismiss();
toast.error("Failed to join this game"); toast.error("Failed to join this game");
} }
} catch (error) {
setSelectedBet(null); // Close modal console.error("Error joining bet:", error);
toast.error("Failed to join this game");
} finally {
setIsProcessing(false);
setSelectedBet(null);
}
}; };
const updateBets = async () => { const updateBets = async () => {
@ -150,14 +159,49 @@ export default function YourGames({ bets }: GameModalProps) {
{/* Modal */} {/* Modal */}
{selectedBet && ( {selectedBet && (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"> <div className="fixed inset-0 flex items-center justify-center z-50 backdrop-blur-sm bg-black/50">
<div className="bg-gray-900 text-white p-6 rounded-lg shadow-lg max-w-sm w-full"> <div className="bg-[rgb(30,30,30)] text-white p-6 rounded-lg shadow-lg max-w-sm w-full">
{isProcessing ? (
<div className="flex flex-col items-center">
<svg
className="animate-spin h-8 w-8 text-blue-400 mb-4"
viewBox="0 0 24 24"
fill="none"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v8H4z"
></path>
</svg>
<h3 className="text-lg font-semibold">Processing...</h3>
<p className="text-gray-400 text-sm mt-2">Joining the bet, please wait...</p>
</div>
) : (
<>
<h2 className="text-xl font-bold mb-4">Are you sure to join this bet?</h2> <h2 className="text-xl font-bold mb-4">Are you sure to join this bet?</h2>
<p><strong>Wager:</strong> {selectedBet.wager} SOL | <strong>Prize:</strong> {(selectedBet.wager * 2).toFixed(2)} SOL</p> <div className="flex gap-4 mb-4">
<p><strong>Game:</strong> {games.find(g => g.id === selectedBet.id)?.name}</p> <div className="w-1/2 relative h-48 overflow-hidden rounded-xl">
<div className="flex items-center mt-3"> <Image
src={games.find(g => g.id === selectedBet.id)?.thumbnail || ''}
alt={games.find(g => g.id === selectedBet.id)?.name || ''}
layout="fill"
objectFit="cover"
/>
</div>
<div className="w-1/2 flex flex-col justify-center space-y-4">
{selectedBet.ownerProfile && ( {selectedBet.ownerProfile && (
<> <>
<p className="text-gray-400 mb-2">Offered by:</p>
<div className="flex items-center">
<Image <Image
src={selectedBet.ownerProfile.x_profile_url || `https://vps.playpoolstudios.com/duelfi/profile_pics/${selectedBet.ownerProfile.id}.jpg`} src={selectedBet.ownerProfile.x_profile_url || `https://vps.playpoolstudios.com/duelfi/profile_pics/${selectedBet.ownerProfile.id}.jpg`}
alt={selectedBet.ownerProfile.username} alt={selectedBet.ownerProfile.username}
@ -165,14 +209,38 @@ export default function YourGames({ bets }: GameModalProps) {
height={32} height={32}
className="w-8 h-8 rounded-full mr-2" className="w-8 h-8 rounded-full mr-2"
/> />
<p><strong>Offered by:</strong> {selectedBet.ownerProfile.username}</p> <p className="font-bold">{selectedBet.ownerProfile.username}</p>
</div>
</> </>
)} )}
<div className="flex justify-between items-center">
<span className="text-gray-400">Wager:</span>
<span className="font-bold">{selectedBet.wager} SOL</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">Prize:</span>
<span className="font-bold">{(selectedBet.wager * 2).toFixed(2)} SOL</span>
</div>
</div>
</div> </div>
<div className="flex justify-end gap-3 mt-4"> <div className="flex justify-end gap-3 mt-4">
<button onClick={() => setSelectedBet(null)} className="bg-gray-700 px-4 py-2 rounded">Cancel</button> <button
<button onClick={handleJoinGame} className="bg-blue-500 px-4 py-2 rounded">Confirm</button> onClick={() => setSelectedBet(null)}
className="bg-gray-700 px-4 py-2 rounded hover:scale-105 transition-all duration-300"
disabled={isProcessing}
>
Cancel
</button>
<button
onClick={handleJoinGame}
className="bg-[rgb(248,144,22)] text-black font-bold px-4 py-2 rounded hover:scale-105 transition-all duration-300"
disabled={isProcessing}
>
Confirm
</button>
</div> </div>
</>
)}
</div> </div>
</div> </div>
)} )}

View File

@ -1,3 +1,4 @@
import { useState, useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
interface PriceSelectionProps { interface PriceSelectionProps {
@ -6,27 +7,74 @@ interface PriceSelectionProps {
} }
export function PriceSelection({ selectedPrice, onSelect }: PriceSelectionProps) { export function PriceSelection({ selectedPrice, onSelect }: PriceSelectionProps) {
const prices = [0.2, 0.5, 0.8]; const presets = [0.05, 0.1, 0.2, 0.5, 1.0];
const [inputValue, setInputValue] = useState<string>("");
const MIN_AMOUNT = 0.05;
useEffect(() => {
if (selectedPrice !== null) {
setInputValue(selectedPrice.toString());
}
}, [selectedPrice]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setInputValue(value);
const parsed = parseFloat(value);
if (!isNaN(parsed) && parsed >= MIN_AMOUNT) {
onSelect(parsed);
}
};
const handlePresetClick = (value: number) => {
onSelect(value);
setInputValue(value.toString());
};
return ( return (
<div className="mb-6 space-y-2"> <div className="mb-6 space-y-2">
{prices.map((price) => ( <label className="block text-sm text-gray-300 font-mono mb-1">Select Currency</label>
<p
key={price} <select
className={`flex items-center gap-2 text-gray-400 font-mono cursor-pointer transition-transform duration-500 className="bg-[rgb(10,10,10)] border border-gray-700 text-[rgb(248,144,22)] font-mono px-2 py-1 rounded-lg w-full cursor-pointer"
${selectedPrice === price ? "text-white translate-x-2" : ""}`}
onClick={() => onSelect(price)}
> >
<Image <option>SOL</option>
src="/duelfiassets/solana logo.png" </select>
alt="SOL" <label className="block text-sm text-gray-300 font-mono mb-1">Enter Price (SOL)</label>
width={16} <div className="flex items-center gap-2">
height={16} <input
className="inline" type="number"
step="0.01"
value={inputValue}
onChange={handleInputChange}
className="bg-[rgb(10,10,10)] border border-gray-700 text-[rgb(248,144,22)] font-mono px-2 py-1 rounded-lg flex-1 appearance-none no-spinner"
placeholder="e.g. 0.5"
/> />
{price} SOL </div>
</p> <div className="flex items-center gap-2">
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[rgb(248,144,22)]">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<div className="flex gap-2 mt-2">
{presets.map((price) => (
<button
key={price}
onClick={() => handlePresetClick(price)}
className={`px-3 py-1 rounded font-mono text-sm border transition
${selectedPrice === price
? "bg-[rgb(248,144,22)] text-black font-bold border-white scale-105"
: "bg-[rgb(10,10,10)] text-white border-gray-600 hover:bg-[rgb(30,30,30)] hover:scale-105"
}`}
>
{price}
</button>
))} ))}
</div> </div>
</div>
); );
} }

View File

@ -6,19 +6,22 @@ export const games = [
name: "Block Drop", name: "Block Drop",
entryFee: "0.1 SOL", entryFee: "0.1 SOL",
thumbnail: "/duelfiassets/Block Drop Illustration.jpeg", thumbnail: "/duelfiassets/Block Drop Illustration.jpeg",
isAvailable: true
}, },
{ {
id: "snakes", id: "snakes",
name: "Venom Trails", name: "Venom Trails",
entryFee: "0.02 ETH", entryFee: "0.02 ETH",
thumbnail: "/duelfiassets/Venom Trail Game Cover Illustration.png", thumbnail: "/duelfiassets/Venom Trail Game Cover Illustration.png",
isAvailable: false
}, },
{ {
id: "bubbles", id: "bubbles",
name: "Wall Smash", name: "Wall Smash",
entryFee: "0.05 ETH", entryFee: "0.05 ETH",
thumbnail: "/duelfiassets/Wall Smash Game Cover Illustration.png", thumbnail: "/duelfiassets/Wall Smash Game Cover Illustration.png",
}, isAvailable: false
}
]; ];
export function GetGameByID(id:string):Game | undefined{ export function GetGameByID(id:string):Game | undefined{

View File

@ -4,3 +4,7 @@ export const FEE_COLLECTOR_PUBKEY = new PublicKey("cocD4r4yNpHxPq7CzUebxEMyLki3X
export const WAGER_PRIZE_MULT = 0.95; export const WAGER_PRIZE_MULT = 0.95;
export const CONFIRMATION_THRESHOLD:Commitment = 'finalized'; export const CONFIRMATION_THRESHOLD:Commitment = 'finalized';
export const URL_TELEGRAM = "https://t.me/duelfidotio";
export const URL_TWITTER = "https://x.com/duelfidotio";
export const URL_WHITEPAPER = "https://duelfi-io.gitbook.io/whitepaper";

View File

@ -3,4 +3,5 @@ export interface Game {
name: string; name: string;
entryFee: string; entryFee: string;
thumbnail: string; thumbnail: string;
isAvailable: boolean;
} }