added new ui
This commit is contained in:
parent
df220a533f
commit
42350c9acc
@ -156,7 +156,9 @@ export default function Boxes() {
|
||||
['left', 'right'].map((side) => (
|
||||
<span
|
||||
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">
|
||||
<path d={pos === 'top' ? 'M1 0V4.5' : 'M1 4.5V0'} stroke="#0CE77E" />
|
||||
|
@ -11,3 +11,26 @@ textarea {
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
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;
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
Connection,
|
||||
SystemProgram,
|
||||
} from '@solana/web3.js';
|
||||
} from "@solana/web3.js";
|
||||
import axios from 'axios';
|
||||
import { usePrivy } from '@privy-io/react-auth';
|
||||
import {
|
||||
useSolanaWallets,
|
||||
} from '@privy-io/react-auth/solana';
|
||||
import { useSolanaWallets } from '@privy-io/react-auth/solana';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import {
|
||||
getAssociatedTokenAddress,
|
||||
createTransferInstruction,
|
||||
TOKEN_PROGRAM_ID,
|
||||
} from "@solana/spl-token";
|
||||
|
||||
interface GpuPaymentModalProps {
|
||||
isOpen: boolean;
|
||||
@ -21,15 +26,197 @@ interface GpuPaymentModalProps {
|
||||
id: string;
|
||||
title: string;
|
||||
price_usd: number;
|
||||
price_per_hour: string; // new field
|
||||
price_per_hour: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!;
|
||||
const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!;
|
||||
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) => {
|
||||
const { user, connectWallet } = usePrivy();
|
||||
const { wallets: privyWallets } = useSolanaWallets();
|
||||
@ -39,12 +226,15 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = 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 userEmail = user?.email?.address || emailInput || null;
|
||||
|
||||
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
const [emailTouched, setEmailTouched] = useState(false);
|
||||
const [selectedToken, setSelectedToken] = useState<'SOL' | 'USDC' | 'JUP'>('SOL');
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
@ -67,6 +257,8 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
||||
fetchSolPrice();
|
||||
setErrorMsg(null);
|
||||
setSuccessMsg(null);
|
||||
setPaymentSuccess(false);
|
||||
setPaymentFailure(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@ -77,155 +269,360 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
||||
|
||||
try {
|
||||
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];
|
||||
|
||||
if (!solWallet || !solWallet.address) {
|
||||
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.');
|
||||
if (!solWallet?.address || !solWallet.signTransaction) {
|
||||
throw new Error("No connected Solana wallet found or wallet can't sign.");
|
||||
}
|
||||
|
||||
const fromPubKey = new PublicKey(solWallet.address);
|
||||
const toPubKey = new PublicKey(BUSINESS_WALLET);
|
||||
const transaction = new Transaction();
|
||||
let txId = "";
|
||||
let tokenUsed = selectedToken;
|
||||
let amountDisplay = "";
|
||||
|
||||
const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6);
|
||||
if (selectedToken === "SOL") {
|
||||
const { data } = await axios.get(
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd"
|
||||
);
|
||||
const solPrice = data.solana.usd;
|
||||
const solAmount = (gpu.price_usd / solPrice).toFixed(6);
|
||||
amountDisplay = `${solAmount} SOL`;
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
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;
|
||||
const { blockhash } = await connection.getLatestBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
|
||||
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, {
|
||||
email: userEmail,
|
||||
product: gpu.title,
|
||||
price_hour: gpu.price_per_hour,
|
||||
price: gpu.price_usd.toFixed(2),
|
||||
token: selectedToken,
|
||||
});
|
||||
|
||||
// Store order in Supabase
|
||||
const { error } = await supabase.from('orders').insert([
|
||||
// Generate a random order ID (in production this would come from the backend)
|
||||
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,
|
||||
amount_sol: parseFloat(solAmount),
|
||||
sol_tx_signature: txId,
|
||||
status: 'success',
|
||||
amount_usd: gpu.price_usd,
|
||||
token_used: tokenUsed,
|
||||
token_amount: amountDisplay,
|
||||
token_tx_signature: txId,
|
||||
status: "success",
|
||||
order_id: generatedOrderId,
|
||||
},
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to save order: ${error.message}`);
|
||||
}
|
||||
|
||||
|
||||
if (error) throw new Error(`Failed to save order: ${error.message}`);
|
||||
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
||||
setPaymentSuccess(true);
|
||||
} 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 {
|
||||
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;
|
||||
|
||||
const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...';
|
||||
|
||||
// Show failure screen if payment failed
|
||||
if (paymentFailure) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div className="bg-white w-full max-w-md rounded-xl p-6 shadow-xl">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{gpu.title}</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Price: <span className="font-medium">${gpu.price_usd} USD</span> (~{solAmount} SOL)
|
||||
</p>
|
||||
<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">
|
||||
<RedCornerEdge position="left-top" />
|
||||
<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 && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wallet not connected */}
|
||||
{privyWallets.length === 0 ? (
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-700 mb-4">
|
||||
To proceed with payment, please connect a Solana wallet.
|
||||
</p>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-between gap-3 mt-2">
|
||||
{/* Confirm Button */}
|
||||
<button
|
||||
onClick={connectWallet}
|
||||
className="px-4 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition"
|
||||
onClick={handlePayment}
|
||||
disabled={loading || (!user?.email?.address && (!emailInput || !isValidEmail(emailInput)))}
|
||||
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)]"
|
||||
>
|
||||
Connect Wallet
|
||||
<CornerEdge position="left-top" />
|
||||
<CornerEdge position="right-top" />
|
||||
<CornerEdge position="left-bottom" />
|
||||
<CornerEdge position="right-bottom" />
|
||||
{loading ? 'PROCESSING...' : 'CONFIRM PAYMENT'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Show email prompt if not logged in with email */}
|
||||
{!user?.email?.address && (
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm text-gray-700 mb-1">
|
||||
Email for invoice & access:
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={emailInput}
|
||||
onChange={(e) => setEmailInput(e.target.value)}
|
||||
onBlur={() => setEmailTouched(true)}
|
||||
className="w-full px-3 py-2 border rounded-md text-sm"
|
||||
/>
|
||||
{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">
|
||||
{/* Cancel Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition"
|
||||
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)]"
|
||||
>
|
||||
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'}
|
||||
<CornerEdge position="left-top" />
|
||||
<CornerEdge position="right-top" />
|
||||
<CornerEdge position="left-bottom" />
|
||||
<CornerEdge position="right-bottom" />
|
||||
CANCEL
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user