added new ui

This commit is contained in:
shialoth 2025-05-04 07:28:42 +05:30
parent df220a533f
commit 42350c9acc
3 changed files with 535 additions and 113 deletions

View File

@ -156,7 +156,9 @@ export default function Boxes() {
['left', 'right'].map((side) => ( ['left', 'right'].map((side) => (
<span <span
key={`${pos}-${side}`} key={`${pos}-${side}`}
className={`absolute -${pos}-px -${side}-px opacity-25 group-hover:opacity-100 duration-200`} className={`absolute -${pos}-px -${side}-px opacity-25 group-hover:opacity-100 duration-200 ${
side === 'right' ? 'scale-x-[-1]' : ''
}`}
> >
<svg width="6" height="5" viewBox="0 0 6 5" fill="none"> <svg width="6" height="5" viewBox="0 0 6 5" fill="none">
<path d={pos === 'top' ? 'M1 0V4.5' : 'M1 4.5V0'} stroke="#0CE77E" /> <path d={pos === 'top' ? 'M1 0V4.5' : 'M1 4.5V0'} stroke="#0CE77E" />

View File

@ -11,3 +11,26 @@ textarea {
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
.token-dropdown {
width: 100%;
padding: 10px 12px;
background: #0E1618;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 3px;
color: #FFFFFF;
font-family: 'Geist Mono', monospace;
font-size: 14px;
line-height: 18px;
letter-spacing: -0.01em;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%230CE77E' stroke-width='1.5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 12px;
}
.token-dropdown:focus {
outline: none;
border-color: #0CE77E;
}

View File

@ -1,18 +1,23 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import React from "react";
import { import {
Connection,
PublicKey, PublicKey,
Transaction, Transaction,
sendAndConfirmTransaction,
Connection,
SystemProgram, SystemProgram,
} from '@solana/web3.js'; } from "@solana/web3.js";
import axios from 'axios'; import axios from 'axios';
import { usePrivy } from '@privy-io/react-auth'; import { usePrivy } from '@privy-io/react-auth';
import { import { useSolanaWallets } from '@privy-io/react-auth/solana';
useSolanaWallets,
} from '@privy-io/react-auth/solana';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import {
getAssociatedTokenAddress,
createTransferInstruction,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
interface GpuPaymentModalProps { interface GpuPaymentModalProps {
isOpen: boolean; isOpen: boolean;
@ -21,15 +26,197 @@ interface GpuPaymentModalProps {
id: string; id: string;
title: string; title: string;
price_usd: number; price_usd: number;
price_per_hour: string; // new field price_per_hour: string;
}; };
} }
const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!; const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!;
const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!; const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!;
const EMAIL_API_URL = process.env.NEXT_PUBLIC_EMAIL_API_URL!; const EMAIL_API_URL = process.env.NEXT_PUBLIC_EMAIL_API_URL!;
const USDC_MINT = new PublicKey("Es9vMFrzaCERJ8gLhEvX5yQceQ2uKcXfUrx2Wcikgqay");
const JUP_MINT = new PublicKey("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN");
const CornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => {
const paths = {
'left-top': (
<>
<path d="M1 0V4.5" stroke="#0CE77E" strokeWidth="1" />
<path d="M5.5 0.5L1 0.5" stroke="#0CE77E" strokeWidth="1" />
</>
),
'right-top': (
<>
<path d="M5 0V4.5" stroke="#0CE77E" strokeWidth="1" />
<path d="M0.5 0.5L5 0.5" stroke="#0CE77E" strokeWidth="1" />
</>
),
'left-bottom': (
<>
<path d="M1 4.5V0" stroke="#0CE77E" strokeWidth="1" />
<path d="M5.5 4L1 4" stroke="#0CE77E" strokeWidth="1" />
</>
),
'right-bottom': (
<>
<path d="M5 4.5V0" stroke="#0CE77E" strokeWidth="1" />
<path d="M0.5 4L5 4" stroke="#0CE77E" strokeWidth="1" />
</>
)
};
return (
<svg
width="6"
height="5"
viewBox="0 0 6 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`absolute ${position.includes('left') ? 'left-0' : 'right-0'} ${position.includes('top') ? 'top-0' : 'bottom-0'}`}
>
{paths[position]}
</svg>
);
};
// Red corner edges for error UI
const RedCornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => {
const paths = {
'left-top': (
<>
<path d="M1 0V4.5" stroke="#F62727" strokeWidth="1" />
<path d="M5.5 0.5L1 0.5" stroke="#F62727" strokeWidth="1" />
</>
),
'right-top': (
<>
<path d="M5 0V4.5" stroke="#F62727" strokeWidth="1" />
<path d="M0.5 0.5L5 0.5" stroke="#F62727" strokeWidth="1" />
</>
),
'left-bottom': (
<>
<path d="M1 4.5V0" stroke="#F62727" strokeWidth="1" />
<path d="M5.5 4L1 4" stroke="#F62727" strokeWidth="1" />
</>
),
'right-bottom': (
<>
<path d="M5 4.5V0" stroke="#F62727" strokeWidth="1" />
<path d="M0.5 4L5 4" stroke="#F62727" strokeWidth="1" />
</>
)
};
return (
<svg
width="6"
height="5"
viewBox="0 0 6 5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`absolute ${position.includes('left') ? 'left-0' : 'right-0'} ${position.includes('top') ? 'top-0' : 'bottom-0'}`}
>
{paths[position]}
</svg>
);
};
const SuccessIcon = () => {
return (
<div className="relative h-[54px] w-[54px]">
<div className="absolute top-0 left-0 bg-emerald-500 rounded-xl h-[54px] w-[54px]" />
<svg
width="39"
height="38"
viewBox="0 0 39 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute top-2 left-2"
aria-hidden="true"
>
<path
d="M32.1663 9.5L14.7497 26.9167L6.83301 19"
stroke="black"
strokeWidth="3.16667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
);
};
const ErrorIcon = () => {
return (
<div className="relative h-[54px] w-[54px]">
<div className="absolute bg-red-600 rounded-xl h-[54px] w-[54px]" />
<svg
width="39"
height="38"
viewBox="0 0 39 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute w-[38px] h-[38px] left-[9px] top-[8px]"
>
<path
d="M29 9.5L10 28.5M10 9.5L29 28.5"
stroke="black"
strokeWidth="3.16667"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
);
};
const ErrorBox = () => {
return (
<div className="relative w-full max-w-[357px] h-[60px]">
<div className="absolute inset-0 bg-[#F62727] bg-opacity-10" />
<div className="absolute inset-[1px] border border-white border-opacity-5" />
<div className="absolute flex justify-between items-center px-5 w-full h-full">
<span className="font-mono text-[18px] font-medium text-white">
Error
</span>
<span className="font-mono text-[18px] font-medium text-[#F62727]">
404 (Payment Error)
</span>
</div>
<RedCornerEdge position="left-top" />
<RedCornerEdge position="right-top" />
<RedCornerEdge position="left-bottom" />
<RedCornerEdge position="right-bottom" />
</div>
);
};
interface OrderIdBoxProps {
orderId: string;
}
const OrderIdBox = ({ orderId }: OrderIdBoxProps) => {
return (
<div className="relative w-full max-w-[357px] h-[60px]">
<div className="absolute inset-0 bg-[#0CE77E] bg-opacity-10" />
<div className="absolute inset-[1px] border border-white border-opacity-5" />
<div className="absolute flex justify-between items-center px-5 w-full h-full">
<span className="font-mono text-[18px] font-medium text-white">
Order ID
</span>
<span className="font-mono text-[18px] font-medium text-[#0CE77E]">
{orderId}
</span>
</div>
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
</div>
);
};
export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => { export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => {
const { user, connectWallet } = usePrivy(); const { user, connectWallet } = usePrivy();
const { wallets: privyWallets } = useSolanaWallets(); const { wallets: privyWallets } = useSolanaWallets();
@ -39,12 +226,15 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [successMsg, setSuccessMsg] = useState<string | null>(null); const [successMsg, setSuccessMsg] = useState<string | null>(null);
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [paymentFailure, setPaymentFailure] = useState(false);
const [orderId, setOrderId] = useState<string>('');
const connection = new Connection(SOLANA_RPC); const connection = new Connection(SOLANA_RPC);
const userEmail = user?.email?.address || emailInput || null; const userEmail = user?.email?.address || emailInput || null;
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const [emailTouched, setEmailTouched] = useState(false); const [emailTouched, setEmailTouched] = useState(false);
const [selectedToken, setSelectedToken] = useState<'SOL' | 'USDC' | 'JUP'>('SOL');
const supabase = createClient( const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_URL!,
@ -67,6 +257,8 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
fetchSolPrice(); fetchSolPrice();
setErrorMsg(null); setErrorMsg(null);
setSuccessMsg(null); setSuccessMsg(null);
setPaymentSuccess(false);
setPaymentFailure(false);
} }
}, [isOpen]); }, [isOpen]);
@ -77,158 +269,363 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
try { try {
if (!userEmail || !isValidEmail(userEmail)) { if (!userEmail || !isValidEmail(userEmail)) {
throw new Error('Please enter a valid email address for your receipt.'); throw new Error("Please enter a valid email address for your receipt.");
} }
const solWallet = privyWallets[0]; const solWallet = privyWallets[0];
if (!solWallet?.address || !solWallet.signTransaction) {
if (!solWallet || !solWallet.address) { throw new Error("No connected Solana wallet found or wallet can't sign.");
throw new Error('No connected Solana wallet found. Please connect a wallet via Privy.');
}
if (!solWallet.signTransaction) {
throw new Error('Connected wallet does not support transaction signing.');
} }
const fromPubKey = new PublicKey(solWallet.address); const fromPubKey = new PublicKey(solWallet.address);
const toPubKey = new PublicKey(BUSINESS_WALLET); const toPubKey = new PublicKey(BUSINESS_WALLET);
const transaction = new Transaction();
const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6); let txId = "";
let tokenUsed = selectedToken;
let amountDisplay = "";
const transaction = new Transaction().add( if (selectedToken === "SOL") {
SystemProgram.transfer({ const { data } = await axios.get(
fromPubkey: fromPubKey, "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd"
toPubkey: toPubKey, );
lamports: Math.floor(parseFloat(solAmount) * 1e9), const solPrice = data.solana.usd;
}) const solAmount = (gpu.price_usd / solPrice).toFixed(6);
); amountDisplay = `${solAmount} SOL`;
transaction.add(
SystemProgram.transfer({
fromPubkey: fromPubKey,
toPubkey: toPubKey,
lamports: Math.floor(parseFloat(solAmount) * 1e9),
})
);
} else {
const selectedMint = selectedToken === "USDC" ? USDC_MINT : JUP_MINT;
const tokenDecimals = 6;
let tokenPrice = 1;
if (selectedToken === "JUP") {
const { data } = await axios.get(
"https://api.coingecko.com/api/v3/simple/price?ids=jupiter-exchange&vs_currencies=usd"
);
tokenPrice = data["jupiter-exchange"]?.usd ?? 0.5;
}
const tokenAmount = (gpu.price_usd / tokenPrice).toFixed(tokenDecimals);
amountDisplay = `${tokenAmount} ${selectedToken}`;
const fromTokenAccount = await getAssociatedTokenAddress(selectedMint, fromPubKey);
const toTokenAccount = await getAssociatedTokenAddress(selectedMint, toPubKey);
transaction.add(
createTransferInstruction(
fromTokenAccount,
toTokenAccount,
fromPubKey,
BigInt(Math.floor(parseFloat(tokenAmount) * Math.pow(10, tokenDecimals))),
[],
TOKEN_PROGRAM_ID
)
);
}
transaction.feePayer = fromPubKey; transaction.feePayer = fromPubKey;
const { blockhash } = await connection.getLatestBlockhash(); const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash; transaction.recentBlockhash = blockhash;
const signedTx = await solWallet.signTransaction(transaction); const signedTx = await solWallet.signTransaction(transaction);
txId = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(txId, "confirmed");
const txId = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(txId, 'confirmed');
// Send confirmation email
await axios.post(EMAIL_API_URL, { await axios.post(EMAIL_API_URL, {
email: userEmail, email: userEmail,
product: gpu.title, product: gpu.title,
price_hour: gpu.price_per_hour, price_hour: gpu.price_per_hour,
price: gpu.price_usd.toFixed(2), price: gpu.price_usd.toFixed(2),
}); token: selectedToken,
});
// Store order in Supabase // Generate a random order ID (in production this would come from the backend)
const { error } = await supabase.from('orders').insert([ const generatedOrderId = `${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}`;
setOrderId(generatedOrderId);
const { error } = await supabase.from("orders").insert([
{ {
gpu_id: gpu.id, gpu: `${gpu.id} ${gpu.title}`,
user_email: userEmail, user_email: userEmail,
amount_sol: parseFloat(solAmount), amount_usd: gpu.price_usd,
sol_tx_signature: txId, token_used: tokenUsed,
status: 'success', token_amount: amountDisplay,
token_tx_signature: txId,
status: "success",
order_id: generatedOrderId,
}, },
]); ]);
if (error) { if (error) throw new Error(`Failed to save order: ${error.message}`);
throw new Error(`Failed to save order: ${error.message}`);
}
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`); setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
setPaymentSuccess(true);
} catch (err: any) { } catch (err: any) {
setErrorMsg(err.message || 'Payment failed.'); console.error("Payment error:", err);
setErrorMsg(err.message || "Payment failed.");
setPaymentFailure(true);
// Log the failed attempt in Supabase
const failedOrderId = `FAIL-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}`;
await supabase.from("orders").insert([
{
gpu: `${gpu.id} ${gpu.title}`,
user_email: userEmail,
amount_usd: gpu.price_usd,
token_used: selectedToken,
status: "failed",
error_message: err.message || "Unknown error",
order_id: failedOrderId,
},
]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleRetry = () => {
// Reset failure state and error message
setPaymentFailure(false);
setErrorMsg(null);
// Don't reset other form values so user doesn't have to start over
};
if (!isOpen) return null; if (!isOpen) return null;
const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...'; const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...';
return ( // Show failure screen if payment failed
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> if (paymentFailure) {
<div className="bg-white w-full max-w-md rounded-xl p-6 shadow-xl"> return (
<h2 className="text-xl font-semibold text-gray-800">{gpu.title}</h2> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<p className="text-gray-600 mt-1"> <div className="relative flex flex-col gap-8 items-center p-6 bg-[#0E1618] border border-white border-opacity-10 w-full max-w-lg mx-4">
Price: <span className="font-medium">${gpu.price_usd} USD</span> (~{solAmount} SOL) <RedCornerEdge position="left-top" />
</p> <RedCornerEdge position="right-top" />
<RedCornerEdge position="left-bottom" />
<RedCornerEdge position="right-bottom" />
<ErrorIcon />
<div className="flex flex-col gap-1.5 items-center">
<h1 className="text-2xl text-center text-white">
PURCHASE FAILED
</h1>
<p className="text-lg text-center text-white opacity-50">
Your purchase didn't go through
</p>
</div>
<ErrorBox />
<div className="flex gap-4">
<button
onClick={handleRetry}
className="relative w-[157px] h-[51px]"
>
<div className="absolute inset-0 bg-[#F62727] bg-opacity-25" />
<div className="absolute inset-[1px] border border-white border-opacity-5" />
<span className="absolute inset-0 flex items-center justify-center font-mono text-[17.85px] font-medium text-[#F62727]">
RETRY
</span>
<RedCornerEdge position="left-top" />
<RedCornerEdge position="right-top" />
<RedCornerEdge position="left-bottom" />
<RedCornerEdge position="right-bottom" />
</button>
<button
onClick={onClose}
className="relative w-[157px] h-[51px]"
>
<div className="absolute inset-0 bg-[#333333]" />
<div className="absolute inset-[1px] border border-white border-opacity-5" />
<span className="absolute inset-0 flex items-center justify-center font-mono text-[17.85px] font-medium text-white">
CLOSE
</span>
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
</button>
</div>
</div>
</div>
);
}
// Show success screen if payment was successful
if (paymentSuccess) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div className="relative flex flex-col gap-8 items-center p-6 bg-[#0E1618] border border-white border-opacity-10 w-full max-w-lg mx-4">
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
<SuccessIcon />
<div className="flex flex-col gap-1.5 items-center">
<h1 className="text-2xl text-center text-white">
PURCHASE SUCCESSFUL
</h1>
<p className="text-lg text-center text-white opacity-50">
Thank you for shopping with Vertex
</p>
</div>
<OrderIdBox orderId={orderId} />
<button
onClick={onClose}
className="relative w-[157px] h-[51px]"
>
<div className="absolute inset-0 bg-[#0CE77E] bg-opacity-25" />
<div className="absolute inset-[1px] border border-white border-opacity-5" />
<span className="absolute inset-0 flex items-center justify-center font-mono text-[17.85px] font-medium text-[#0CE77E]">
CLOSE
</span>
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
</button>
</div>
</div>
);
}
// Show payment form
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div className="relative flex flex-col gap-5 p-6 bg-[#0E1618] border border-white border-opacity-10 w-full max-w-lg mx-4">
{/* Corner decoration elements */}
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
{/* Header */}
<div className="flex flex-col gap-1">
<h2 className="text-sm font-medium text-white text-opacity-50">
Selected configuration
</h2>
<h1 className="text-xl font-medium text-[#0CE77E]">
{gpu.title}
</h1>
</div>
{/* Price Display */}
<div className="relative border border-white border-opacity-10 bg-[#0CE77E]/10">
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
<div className="flex justify-between items-center p-3 w-full">
<span className="text-base font-medium text-white">Total Price</span>
<div className="flex gap-3 items-center">
<span className="text-base font-bold text-[#0CE77E]">${gpu.price_usd}</span>
<span className="text-base text-[#0CE77E]">(~{solAmount} SOL)</span>
</div>
</div>
</div>
{/* Email Input */}
{!user?.email?.address && (
<div className="relative">
<span className="block mb-1 text-sm font-medium text-white">YOUR EMAIL ADDRESS</span>
<div className="relative border border-white border-opacity-10 bg-[#0CE77E]/10">
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
<input
type="email"
placeholder="ENTER YOUR EMAIL"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
onBlur={() => setEmailTouched(true)}
className="w-full p-3 bg-transparent text-[#0CE77E] placeholder-[#0CE77E]/50 focus:outline-none text-base"
/>
</div>
{emailTouched && emailInput && !isValidEmail(emailInput) && (
<p className="mt-1 text-xs text-red-400">Please enter a valid email address.</p>
)}
</div>
)}
{/* Token Selection */}
<div className="relative">
<span className="block mb-1 text-sm font-medium text-white">SELECT PAYMENT TOKEN</span>
<div className="relative border border-white border-opacity-10 bg-[#0CE77E]/10">
<CornerEdge position="left-top" />
<CornerEdge position="right-top" />
<CornerEdge position="left-bottom" />
<CornerEdge position="right-bottom" />
<select
value={selectedToken}
onChange={(e) => setSelectedToken(e.target.value as 'SOL' | 'USDC' | 'JUP')}
className="w-full p-3 bg-transparent text-[#0CE77E] focus:outline-none text-base cursor-pointer"
>
<option value="SOL" className="bg-[#0E1618] text-[#0CE77E]">SOL</option>
<option value="USDC" className="bg-[#0E1618] text-[#0CE77E]">USDC</option>
<option value="JUP" className="bg-[#0E1618] text-[#0CE77E]">JUP</option>
</select>
</div>
</div>
{/* Error and Success Messages */}
{errorMsg && ( {errorMsg && (
<div className="mt-4 rounded-md bg-red-100 px-4 py-2 text-sm text-red-700"> <div className="p-2 text-sm text-red-400 border border-red-400 bg-red-400/10">
{errorMsg} {errorMsg}
</div> </div>
)} )}
{successMsg && ( {successMsg && (
<div className="mt-4 rounded-md bg-green-100 px-4 py-2 text-sm text-green-700"> <div className="p-2 text-sm text-[#0CE77E] border border-[#0CE77E] bg-[#0CE77E]/10">
{successMsg} {successMsg}
</div> </div>
)} )}
{/* Wallet not connected */} {/* Action Buttons */}
{privyWallets.length === 0 ? ( <div className="flex justify-between gap-3 mt-2">
<div className="mt-6 text-center"> {/* Confirm Button */}
<p className="text-sm text-gray-700 mb-4"> <button
To proceed with payment, please connect a Solana wallet. onClick={handlePayment}
</p> disabled={loading || (!user?.email?.address && (!emailInput || !isValidEmail(emailInput)))}
<button className="relative flex-1 py-3 bg-[#004d33] border border-[#0CE77E]/30 text-[#0CE77E] text-sm font-medium disabled:opacity-50 transition-all hover:bg-[#006644] hover:border-[#0CE77E]/50 hover:shadow-[0_0_12px_rgba(12,231,126,0.3)]"
onClick={connectWallet} >
className="px-4 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition" <CornerEdge position="left-top" />
> <CornerEdge position="right-top" />
Connect Wallet <CornerEdge position="left-bottom" />
</button> <CornerEdge position="right-bottom" />
</div> {loading ? 'PROCESSING...' : 'CONFIRM PAYMENT'}
) : ( </button>
<>
{/* Show email prompt if not logged in with email */} {/* Cancel Button */}
{!user?.email?.address && ( <button
<div className="mt-4"> onClick={onClose}
<label className="block text-sm text-gray-700 mb-1"> disabled={loading}
Email for invoice & access: className="relative flex-1 py-3 bg-[#333333] border border-white/30 text-white text-sm font-medium disabled:opacity-50 transition-all hover:bg-[#444444] hover:border-white/50 hover:shadow-[0_0_12px_rgba(255,255,255,0.2)]"
</label> >
<input <CornerEdge position="left-top" />
type="email" <CornerEdge position="right-top" />
placeholder="you@example.com" <CornerEdge position="left-bottom" />
value={emailInput} <CornerEdge position="right-bottom" />
onChange={(e) => setEmailInput(e.target.value)} CANCEL
onBlur={() => setEmailTouched(true)} </button>
className="w-full px-3 py-2 border rounded-md text-sm" </div>
/>
{emailTouched && emailInput && !isValidEmail(emailInput) && (
<p className="mt-1 text-xs text-red-600">Please enter a valid email address.</p>
)}
</div>
)}
<div className="mt-6 flex justify-end gap-3">
<button
onClick={onClose}
disabled={loading}
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition"
>
Cancel
</button>
<button
onClick={handlePayment}
disabled={
loading ||
(!user?.email?.address && (!emailInput || !isValidEmail(emailInput)))
}
className="px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 transition disabled:opacity-60"
>
{loading ? 'Processing...' : 'Confirm Payment'}
</button>
</div>
</>
)}
</div> </div>
</div> </div>
); );
}; };
export default GpuPaymentModal; export default GpuPaymentModal;