polished a bit
This commit is contained in:
parent
147ef9e669
commit
6a4de52d06
Binary file not shown.
|
Before Width: | Height: | Size: 4 B After Width: | Height: | Size: 15 KiB |
|
|
@ -154,3 +154,15 @@ body {
|
|||
opacity: 1;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { URL_TELEGRAM, URL_TWITTER } from "@/shared/constants";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ export default function Footer() {
|
|||
{/* Social Buttons */}
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href="https://t.me/yourchannel"
|
||||
href={URL_TELEGRAM}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-transform duration-300 hover:scale-110"
|
||||
|
|
@ -35,7 +36,7 @@ export default function Footer() {
|
|||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/yourhandle"
|
||||
href={URL_TWITTER}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-transform duration-300 hover:scale-110"
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ export function GameSelection({ games, selectedGame, onSelect }: GameSelectionPr
|
|||
<div
|
||||
key={game.id}
|
||||
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)]"}
|
||||
hover:scale-105 hover:border-gray-400`}
|
||||
onClick={() => onSelect(game)}
|
||||
${selectedGame?.id === game.id
|
||||
? "scale-110 border-gray-400 bg-gray-800"
|
||||
: 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
|
||||
src={game.thumbnail}
|
||||
|
|
@ -26,6 +30,9 @@ export function GameSelection({ games, selectedGame, onSelect }: GameSelectionPr
|
|||
className="mb-2 rounded-md"
|
||||
/>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Image from "next/image";
|
|||
import { FaTelegram, FaXTwitter } from "react-icons/fa6";
|
||||
import { useState } from "react";
|
||||
import PrivyButton from "./PrivyButton";
|
||||
import { URL_TELEGRAM, URL_TWITTER, URL_WHITEPAPER } from "@/shared/constants";
|
||||
|
||||
export default function Header() {
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
|
|
@ -25,7 +26,7 @@ export default function Header() {
|
|||
{/* Navigation Links (hidden on mobile, visible on md+) */}
|
||||
<nav className="hidden md:flex gap-6 ml-6">
|
||||
<Link
|
||||
href="/whitepaper"
|
||||
href={URL_WHITEPAPER}
|
||||
className="text-white transition-colors duration-[500ms] hover:text-[rgb(248,144,22)]"
|
||||
>
|
||||
Whitepaper
|
||||
|
|
@ -44,7 +45,7 @@ export default function Header() {
|
|||
{/* Socials & Login (hidden on mobile, visible on md+) */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
<a
|
||||
href="https://t.me/yourchannel"
|
||||
href={URL_TELEGRAM}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-transform duration-300 hover:scale-120"
|
||||
|
|
@ -57,7 +58,7 @@ export default function Header() {
|
|||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/yourhandle"
|
||||
href={URL_TWITTER}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-transform duration-300 hover:scale-120"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default function YourGames({ bets }: GameModalProps) {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const { user } = usePrivy();
|
||||
const [selectedBet, setSelectedBet] = useState<Bet | null>(null); // Track selected bet
|
||||
const [isProcessing, setIsProcessing] = useState(false); // Track processing state
|
||||
|
||||
const handleJoinGame = async () => {
|
||||
if (!selectedBet) return;
|
||||
|
|
@ -29,27 +30,35 @@ export default function YourGames({ bets }: GameModalProps) {
|
|||
wallet = _wallet;
|
||||
}
|
||||
});
|
||||
toast.loading("Joining Bet");
|
||||
const tx = await joinBet(wallet, user?.id ?? "", selectedBet.id, selectedBet.address);
|
||||
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
|
||||
|
||||
if (tx.length > 5) {
|
||||
connection.confirmTransaction(tx, CONFIRMATION_THRESHOLD).finally(()=>{
|
||||
setIsProcessing(true);
|
||||
toast.loading("Joining Bet");
|
||||
try {
|
||||
const tx = await joinBet(wallet, user?.id ?? "", selectedBet.id, selectedBet.address);
|
||||
const url = EXPLORER_TX_TEMPLATE.replace("{address}", tx);
|
||||
|
||||
if (tx.length > 5) {
|
||||
connection.confirmTransaction(tx, CONFIRMATION_THRESHOLD).finally(()=>{
|
||||
toast.dismiss();
|
||||
toast.success("Joined game successfully!", {
|
||||
action: {
|
||||
label: "View TX",
|
||||
onClick: () => window.open(url, "_blank"),
|
||||
},
|
||||
});
|
||||
})
|
||||
} else {
|
||||
toast.dismiss();
|
||||
|
||||
toast.success("Joined game successfully!", {
|
||||
action: {
|
||||
label: "View TX",
|
||||
onClick: () => window.open(url, "_blank"),
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
} else {
|
||||
toast.error("Failed to join this game");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error joining bet:", error);
|
||||
toast.error("Failed to join this game");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
setSelectedBet(null);
|
||||
}
|
||||
|
||||
setSelectedBet(null); // Close modal
|
||||
};
|
||||
|
||||
const updateBets = async () => {
|
||||
|
|
@ -150,29 +159,88 @@ export default function YourGames({ bets }: GameModalProps) {
|
|||
|
||||
{/* Modal */}
|
||||
{selectedBet && (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||
<div className="bg-gray-900 text-white p-6 rounded-lg shadow-lg max-w-sm w-full">
|
||||
<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>
|
||||
<p><strong>Game:</strong> {games.find(g => g.id === selectedBet.id)?.name}</p>
|
||||
<div className="flex items-center mt-3">
|
||||
{selectedBet.ownerProfile && (
|
||||
<>
|
||||
<Image
|
||||
src={selectedBet.ownerProfile.x_profile_url || `https://vps.playpoolstudios.com/duelfi/profile_pics/${selectedBet.ownerProfile.id}.jpg`}
|
||||
alt={selectedBet.ownerProfile.username}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-8 h-8 rounded-full mr-2"
|
||||
/>
|
||||
<p><strong>Offered by:</strong> {selectedBet.ownerProfile.username}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<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 onClick={handleJoinGame} className="bg-blue-500 px-4 py-2 rounded">Confirm</button>
|
||||
</div>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-50 backdrop-blur-sm bg-black/50">
|
||||
<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>
|
||||
<div className="flex gap-4 mb-4">
|
||||
<div className="w-1/2 relative h-48 overflow-hidden rounded-xl">
|
||||
<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 && (
|
||||
<>
|
||||
<p className="text-gray-400 mb-2">Offered by:</p>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={selectedBet.ownerProfile.x_profile_url || `https://vps.playpoolstudios.com/duelfi/profile_pics/${selectedBet.ownerProfile.id}.jpg`}
|
||||
alt={selectedBet.ownerProfile.username}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-8 h-8 rounded-full mr-2"
|
||||
/>
|
||||
<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 className="flex justify-end gap-3 mt-4">
|
||||
<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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface PriceSelectionProps {
|
||||
|
|
@ -6,27 +7,74 @@ interface 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 (
|
||||
<div className="mb-6 space-y-2">
|
||||
{prices.map((price) => (
|
||||
<p
|
||||
key={price}
|
||||
className={`flex items-center gap-2 text-gray-400 font-mono cursor-pointer transition-transform duration-500
|
||||
${selectedPrice === price ? "text-white translate-x-2" : ""}`}
|
||||
onClick={() => onSelect(price)}
|
||||
>
|
||||
<Image
|
||||
src="/duelfiassets/solana logo.png"
|
||||
alt="SOL"
|
||||
width={16}
|
||||
height={16}
|
||||
className="inline"
|
||||
/>
|
||||
{price} SOL
|
||||
</p>
|
||||
))}
|
||||
<label className="block text-sm text-gray-300 font-mono mb-1">Select Currency</label>
|
||||
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option>SOL</option>
|
||||
</select>
|
||||
<label className="block text-sm text-gray-300 font-mono mb-1">Enter Price (SOL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,19 +6,22 @@ export const games = [
|
|||
name: "Block Drop",
|
||||
entryFee: "0.1 SOL",
|
||||
thumbnail: "/duelfiassets/Block Drop Illustration.jpeg",
|
||||
isAvailable: true
|
||||
},
|
||||
{
|
||||
id: "snakes",
|
||||
name: "Venom Trails",
|
||||
entryFee: "0.02 ETH",
|
||||
thumbnail: "/duelfiassets/Venom Trail Game Cover Illustration.png",
|
||||
isAvailable: false
|
||||
},
|
||||
{
|
||||
id: "bubbles",
|
||||
name: "Wall Smash",
|
||||
entryFee: "0.05 ETH",
|
||||
thumbnail: "/duelfiassets/Wall Smash Game Cover Illustration.png",
|
||||
},
|
||||
isAvailable: false
|
||||
}
|
||||
];
|
||||
|
||||
export function GetGameByID(id:string):Game | undefined{
|
||||
|
|
|
|||
|
|
@ -3,4 +3,8 @@ import { Commitment, PublicKey } from "@solana/web3.js";
|
|||
export const FEE_COLLECTOR_PUBKEY = new PublicKey("cocD4r4yNpHxPq7CzUebxEMyLki3X4d2Y3HcTX5ptUc");
|
||||
export const WAGER_PRIZE_MULT = 0.95;
|
||||
|
||||
export const CONFIRMATION_THRESHOLD:Commitment = '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";
|
||||
|
|
@ -3,4 +3,5 @@ export interface Game {
|
|||
name: string;
|
||||
entryFee: string;
|
||||
thumbnail: string;
|
||||
isAvailable: boolean;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user