Merge branch 'solana-payment'
This commit is contained in:
commit
bf5e8d4ba7
13
package.json
13
package.json
@ -10,12 +10,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@geist-ui/core": "^2.3.8",
|
||||
"@privy-io/react-auth": "^2.9.1",
|
||||
"@solana/spl-token": "^0.4.9",
|
||||
"@privy-io/react-auth": "^2.0.4",
|
||||
"@solana/spl-token": "^0.4.13",
|
||||
"@solana/wallet-adapter-base": "^0.9.26",
|
||||
"@solana/wallet-adapter-phantom": "^0.9.27",
|
||||
"@solana/wallet-adapter-react": "^0.15.38",
|
||||
"@solana/wallet-adapter-react-ui": "^0.9.38",
|
||||
"@solana/web3.js": "^1.98.2",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"axios": "^1.9.0",
|
||||
"chart.js": "^4.4.7",
|
||||
"framer-motion": "^11.18.1",
|
||||
"geist": "^1.3.1",
|
||||
"next": "^14.2.23",
|
||||
"nodemailer": "^6.10.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -25,6 +33,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.0.0",
|
||||
|
@ -1,234 +1,210 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import GpuPaymentModal from '@/components/GpuPaymentModal'
|
||||
|
||||
interface IGPU {
|
||||
id: string;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
price: string;
|
||||
price_usd: number;
|
||||
isRecentlyPurchased?: boolean;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export default function Boxes() {
|
||||
const [selectedGpu, setSelectedGpu] = useState<IGPU | null>(null);
|
||||
|
||||
const gpus: IGPU[] = [
|
||||
{
|
||||
title: "NVIDIA RTX A4000",
|
||||
subTitle: "16GB GDDR6 - 6144 CUDA Cores",
|
||||
price: "$0.20/hour ($147/mo)",
|
||||
id: 'k80',
|
||||
title: 'NVIDIA Tesla K80',
|
||||
subTitle: '24 GB GDDR5 - 4992 CUDA Cores',
|
||||
price: '$0.15/hour ($110/mo)',
|
||||
price_usd: 110,
|
||||
},
|
||||
{
|
||||
id: 't1000',
|
||||
title: 'NVIDIA T1000',
|
||||
subTitle: '4 GB GDDR6 - 896 CUDA Cores',
|
||||
price: '$0.16/hr ($119/month)',
|
||||
price_usd: 119,
|
||||
},
|
||||
{
|
||||
id: 'a4000',
|
||||
title: 'NVIDIA RTX A4000',
|
||||
subTitle: '16 GB GDDR6 - 6144 CUDA Cores',
|
||||
price: '$0.17/hour ($125/mo)',
|
||||
price_usd: 125,
|
||||
isRecentlyPurchased: true,
|
||||
link: "https://nowpayments.io/payment?iid=5964763281",
|
||||
},
|
||||
{
|
||||
title: "NVIDIA RTX A5000",
|
||||
subTitle: "24 GB GDDR6 - 8192 CUDA Cores",
|
||||
price: "$0.26/hr ($190/month)",
|
||||
link: "https://nowpayments.io/payment?iid=4873013025",
|
||||
id: 'a5000',
|
||||
title: 'NVIDIA RTX A5000',
|
||||
subTitle: '24 GB GDDR6 - 8192 CUDA Cores',
|
||||
price: '$0.22/hr ($162/month)',
|
||||
price_usd: 162,
|
||||
},
|
||||
{
|
||||
title: "4x NVIDIA RTX 4090",
|
||||
subTitle: "24 GB GDDR6X - 16384 GPU CUDA Cores",
|
||||
price: "$0.37/hr ($268/month)",
|
||||
link: "https://nowpayments.io/payment?iid=5977605820",
|
||||
id: 'v100',
|
||||
title: 'NVIDIA V100',
|
||||
subTitle: '32 GB HBM2 - 5120 CUDA Cores',
|
||||
price: '$0.23/hr ($170/month)',
|
||||
price_usd: 170,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA V100",
|
||||
subTitle: "32 GB HBM2 - 5120 CUDA Cores",
|
||||
price: "$0.44/hr ($317/month)",
|
||||
link: "https://nowpayments.io/payment?iid=5437270588",
|
||||
id: 'p100',
|
||||
title: 'NVIDIA Tesla P100',
|
||||
subTitle: '16 GB HBM2 - 3584 CUDA Cores',
|
||||
price: '$0.27/hour ($196/mo)',
|
||||
price_usd: 196,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA L4 Ada",
|
||||
subTitle: "24 GB GDDR6 - 7680 CUDA Cores",
|
||||
price: "$0.52/hr ($372/month)",
|
||||
link: "https://nowpayments.io/payment?iid=6283623234",
|
||||
id: '4x4090',
|
||||
title: '4x NVIDIA RTX 4090',
|
||||
subTitle: '24 GB GDDR6X - 16384 GPU CUDA Cores',
|
||||
price: '$0.32/hr ($228/month)',
|
||||
price_usd: 228,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA RTX A6000",
|
||||
subTitle: "48 GB GDDR6 - 10752 CUDA Cores",
|
||||
price: "$0.53/hr ($380/month)",
|
||||
link: "https://nowpayments.io/payment?iid=6074730706",
|
||||
id: 'l4ada',
|
||||
title: 'NVIDIA L4 Ada',
|
||||
subTitle: '24 GB GDDR6 - 7680 CUDA Cores',
|
||||
price: '$0.44/hr ($316/month)',
|
||||
price_usd: 316,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA L40S Ada",
|
||||
subTitle: "48 GB GDDR6 - 18176 CUDA Cores",
|
||||
price: "$1.24/hr ($890/month)",
|
||||
link: "https://nowpayments.io/payment?iid=4570621084",
|
||||
id: 'a6000',
|
||||
title: 'NVIDIA RTX A6000',
|
||||
subTitle: '48 GB GDDR6 - 10752 CUDA Cores',
|
||||
price: '$0.45/hr ($323/month)',
|
||||
price_usd: 323,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA A100 PCIe",
|
||||
subTitle: "40 GB HBM2 - 6912 CUDA Cores",
|
||||
price: "$1.62/hr ($1,166/month)",
|
||||
link: "https://nowpayments.io/payment?iid=6381921922",
|
||||
id: 'l40sada',
|
||||
title: 'NVIDIA L40S Ada',
|
||||
subTitle: '48 GB GDDR6 - 18176 CUDA Cores',
|
||||
price: '$1.04/hr ($756/month)',
|
||||
price_usd: 756,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA H100 NVL",
|
||||
subTitle: "94 GB HBM3 - 14592 CUDA Cores",
|
||||
price: "$3.35/hr ($2,410/month)",
|
||||
link: "https://nowpayments.io/payment?iid=5319698362",
|
||||
id: 'a100',
|
||||
title: 'NVIDIA A100 PCIe',
|
||||
subTitle: '40 GB HBM2 - 6912 CUDA Cores',
|
||||
price: '$1.36/hr ($991/month)',
|
||||
price_usd: 991,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA H100 SXM",
|
||||
subTitle: "80 GB HBM3 - 14592 CUDA Cores",
|
||||
price: "$3.60/hr ($2,592/month)",
|
||||
link: "https://nowpayments.io/payment?iid=4768823499",
|
||||
id: 'h100nvl',
|
||||
title: 'NVIDIA H100 NVL',
|
||||
subTitle: '94 GB HBM3 - 14592 CUDA Cores',
|
||||
price: '$2.85/hr ($2,048/month)',
|
||||
price_usd: 2048,
|
||||
},
|
||||
{
|
||||
id: 'h100sxm',
|
||||
title: 'NVIDIA H100 SXM',
|
||||
subTitle: '80 GB HBM3 - 14592 CUDA Cores',
|
||||
price: '$3.06/hr ($2,203/month)',
|
||||
price_usd: 2203,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{gpus.map((gpu, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{
|
||||
y: 25,
|
||||
opacity: 0,
|
||||
}}
|
||||
whileInView={{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.2 * index,
|
||||
}}
|
||||
viewport={{
|
||||
once: true,
|
||||
}}
|
||||
className="group relative p-5 border border-white/5 bg-[#0E1618]/10 hover:bg-[#0CE77E]/25 backdrop-blur-sm duration-200"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="40" height="40" rx="3" fill="#0CE77E" />
|
||||
<rect
|
||||
width="40"
|
||||
height="40"
|
||||
rx="3"
|
||||
fill="url(#paint0_linear_26_99714)"
|
||||
fillOpacity="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M17.0338 16.8167V15.3963C17.1707 15.3877 17.3162 15.3791 17.4531 15.3791C21.3551 15.2593 23.9051 18.7249 23.9051 18.7249C23.9051 18.7249 21.1498 22.5585 18.189 22.5585C17.7612 22.5585 17.3847 22.49 17.0424 22.3702V18.0489C18.557 18.2372 18.865 18.9046 19.7806 20.4192L21.8087 18.7078C21.8087 18.7078 20.3283 16.7654 17.8296 16.7654C17.5558 16.7739 17.2905 16.7911 17.0338 16.8167ZM17.0338 12.1104V14.2325C17.1707 14.2239 17.3162 14.2154 17.4531 14.2068C22.8697 14.0186 26.4038 18.6479 26.4038 18.6479C26.4038 18.6479 22.3478 23.5853 18.1291 23.5853C17.7441 23.5853 17.3847 23.5511 17.0424 23.4912V24.809C17.3333 24.8432 17.6414 24.8689 17.9494 24.8689C21.8857 24.8689 24.7266 22.858 27.482 20.4877C27.9355 20.8557 29.8095 21.7456 30.1946 22.1306C27.5761 24.3213 21.4749 26.0926 18.0179 26.0926C17.6842 26.0926 17.3676 26.0754 17.0509 26.0412V27.8895H32.0001V12.1104H17.0338ZM17.0338 22.3788V23.4998C13.3971 22.8494 12.3874 19.0672 12.3874 19.0672C12.3874 19.0672 14.133 17.1333 17.0338 16.8167V18.0489H17.0253C15.5107 17.8607 14.3127 19.2897 14.3127 19.2897C14.3127 19.2897 14.9887 21.6771 17.0338 22.3788ZM10.5733 18.9046C10.5733 18.9046 12.7296 15.73 17.0338 15.3963V14.2411C12.2676 14.6261 8.14307 18.665 8.14307 18.665C8.14307 18.665 10.4791 25.4251 17.0338 26.0498V24.8176C12.2248 24.21 10.5733 18.9046 10.5733 18.9046Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_26_99714"
|
||||
x1="20"
|
||||
y1="0"
|
||||
x2="20"
|
||||
y2="40"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopOpacity="0" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<p className="font-medium text-[#0CE77E]">{gpu.title}</p>
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{gpus.map((gpu, index) => (
|
||||
<motion.div
|
||||
key={gpu.id}
|
||||
initial={{ y: 25, opacity: 0 }}
|
||||
whileInView={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2 * index }}
|
||||
viewport={{ once: true }}
|
||||
className="group relative p-5 border border-white/5 bg-[#0E1618]/10 hover:bg-[#0CE77E]/25 backdrop-blur-sm duration-200"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* GPU SVG ICON */}
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="3" fill="#0CE77E" />
|
||||
<rect width="40" height="40" rx="3" fill="url(#gpuGrad)" fillOpacity="0.5" />
|
||||
<path d="M17.0338 16.8167V15.3963C17.1707 15.3877 17.3162 15.3791 17.4531 15.3791C21.3551 15.2593 23.9051 18.7249 23.9051 18.7249C23.9051 18.7249 21.1498 22.5585 18.189 22.5585C17.7612 22.5585 17.3847 22.49 17.0424 22.3702V18.0489C18.557 18.2372 18.865 18.9046 19.7806 20.4192L21.8087 18.7078C21.8087 18.7078 20.3283 16.7654 17.8296 16.7654C17.5558 16.7739 17.2905 16.7911 17.0338 16.8167ZM17.0338 12.1104V14.2325C17.1707 14.2239 17.3162 14.2154 17.4531 14.2068C22.8697 14.0186 26.4038 18.6479 26.4038 18.6479C26.4038 18.6479 22.3478 23.5853 18.1291 23.5853C17.7441 23.5853 17.3847 23.5511 17.0424 23.4912V24.809C17.3333 24.8432 17.6414 24.8689 17.9494 24.8689C21.8857 24.8689 24.7266 22.858 27.482 20.4877C27.9355 20.8557 29.8095 21.7456 30.1946 22.1306C27.5761 24.3213 21.4749 26.0926 18.0179 26.0926C17.6842 26.0926 17.3676 26.0754 17.0509 26.0412V27.8895H32.0001V12.1104H17.0338ZM17.0338 22.3788V23.4998C13.3971 22.8494 12.3874 19.0672 12.3874 19.0672C12.3874 19.0672 14.133 17.1333 17.0338 16.8167V18.0489H17.0253C15.5107 17.8607 14.3127 19.2897 14.3127 19.2897C14.3127 19.2897 14.9887 21.6771 17.0338 22.3788ZM10.5733 18.9046C10.5733 18.9046 12.7296 15.73 17.0338 15.3963V14.2411C12.2676 14.6261 8.14307 18.665 8.14307 18.665C8.14307 18.665 10.4791 25.4251 17.0338 26.0498V24.8176C12.2248 24.21 10.5733 18.9046 10.5733 18.9046Z" fill="white" />
|
||||
<defs>
|
||||
<linearGradient id="gpuGrad" x1="20" y1="0" x2="20" y2="40" gradientUnits="userSpaceOnUse">
|
||||
<stop stopOpacity="0" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<p className="font-medium text-[#0CE77E]">{gpu.title}</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/75">{gpu.subTitle}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-white/75">{gpu.subTitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="p-2.5 border border-white/10 text-sm font-medium text-[#0CE77E]">
|
||||
{gpu.price}
|
||||
</p>
|
||||
|
||||
{gpu.isRecentlyPurchased && (
|
||||
<p className="py-1.5 px-2 border border-[#0CE77E] bg-[#0CE77E]/10 flex items-center gap-1.5 text-xs font-medium text-[#0CE77E]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.58334 1.16675L2.38785 7.40136C2.18438 7.64548 2.08265 7.76757 2.08109 7.87071C2.07973 7.96031 2.11968 8.04559 2.1894 8.10194C2.2696 8.16675 2.42852 8.16675 2.74636 8.16675H7.00001L6.41668 12.8334L11.6121 6.59881C11.8156 6.35468 11.9173 6.23259 11.9189 6.12946C11.9203 6.03986 11.8804 5.95457 11.8106 5.89822C11.7304 5.83342 11.5715 5.83342 11.2537 5.83342H7.00001L7.58334 1.16675Z"
|
||||
fill="#0CE77E"
|
||||
stroke="#0CE77E"
|
||||
strokeWidth="1.16667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Recently purchased
|
||||
<div className="mt-5 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="p-2.5 border border-white/10 text-sm font-medium text-[#0CE77E]">
|
||||
{gpu.price}
|
||||
</p>
|
||||
)}
|
||||
{gpu.isRecentlyPurchased && (
|
||||
<p className="py-1.5 px-2 border border-[#0CE77E] bg-[#0CE77E]/10 flex items-center gap-1.5 text-xs font-medium text-[#0CE77E]">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
d="M7.58334 1.16675L2.38785 7.40136C2.18438 7.64548 2.08265 7.76757 2.08109 7.87071C2.07973 7.96031 2.11968 8.04559 2.1894 8.10194C2.2696 8.16675 2.42852 8.16675 2.74636 8.16675H7.00001L6.41668 12.8334L11.6121 6.59881C11.8156 6.35468 11.9173 6.23259 11.9189 6.12946C11.9203 6.03986 11.8804 5.95457 11.8106 5.89822C11.7304 5.83342 11.5715 5.83342 11.2537 5.83342H7.00001L7.58334 1.16675Z"
|
||||
fill="#0CE77E"
|
||||
stroke="#0CE77E"
|
||||
strokeWidth="1.16667"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Recently purchased
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedGpu(gpu)}
|
||||
className="py-2.5 px-5 border border-[#0CE77E] bg-[#0CE77E]/10 hover:bg-[#0CE77E] text-sm font-medium uppercase text-[#0CE77E] hover:text-[#0B1516] duration-200"
|
||||
>
|
||||
Rent now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={gpu.link}
|
||||
target="_blank"
|
||||
className="py-2.5 px-5 border border-[#0CE77E] bg-[#0CE77E]/10 hover:bg-[#0CE77E] text-sm font-medium uppercase text-[#0CE77E] hover:text-[#0B1516] duration-200"
|
||||
>
|
||||
Rent now
|
||||
</Link>
|
||||
</div>
|
||||
{/* Corner Decorations */}
|
||||
{['top', 'bottom'].map((pos) =>
|
||||
['left', 'right'].map((side) => (
|
||||
<span
|
||||
key={`${pos}-${side}`}
|
||||
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" />
|
||||
<path d={pos === 'top' ? 'M5.5 0.5L1 0.5' : 'M5.5 4L1 4'} stroke="#0CE77E" />
|
||||
</svg>
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<span className="absolute -top-px -left-px opacity-25 group-hover:opacity-100 duration-200">
|
||||
<svg
|
||||
width="6"
|
||||
height="5"
|
||||
viewBox="0 0 6 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 0V4.5" stroke="#0CE77E" />
|
||||
<path d="M5.5 0.5L1 0.5" stroke="#0CE77E" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span className="absolute -bottom-px -left-px opacity-25 group-hover:opacity-100 duration-200">
|
||||
<svg
|
||||
width="6"
|
||||
height="5"
|
||||
viewBox="0 0 6 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M1 4.5V0" stroke="#0CE77E" />
|
||||
<path d="M5.5 4L1 4" stroke="#0CE77E" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span className="absolute -top-px -right-px opacity-25 group-hover:opacity-100 duration-200">
|
||||
<svg
|
||||
width="6"
|
||||
height="5"
|
||||
viewBox="0 0 6 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M5 0V4.5" stroke="#0CE77E" />
|
||||
<path d="M0.5 0.5L5 0.5" stroke="#0CE77E" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span className="absolute -bottom-px -right-px opacity-25 group-hover:opacity-100 duration-200">
|
||||
<svg
|
||||
width="6"
|
||||
height="5"
|
||||
viewBox="0 0 6 5"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M5 4.5V0" stroke="#0CE77E" />
|
||||
<path d="M0.5 4L5 4" stroke="#0CE77E" />
|
||||
</svg>
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
{selectedGpu && (
|
||||
<GpuPaymentModal
|
||||
isOpen={true}
|
||||
onClose={() => setSelectedGpu(null)}
|
||||
gpu={{
|
||||
id: selectedGpu.id,
|
||||
title: selectedGpu.title,
|
||||
price_usd: selectedGpu.price_usd,
|
||||
price_per_hour: selectedGpu.price, // pass it here
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
804
src/components/GpuPaymentModal.tsx
Normal file
804
src/components/GpuPaymentModal.tsx
Normal file
@ -0,0 +1,804 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import React from "react";
|
||||
import {
|
||||
PublicKey,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
Connection,
|
||||
SystemProgram,
|
||||
} from "@solana/web3.js";
|
||||
import axios from 'axios';
|
||||
import { usePrivy } from '@privy-io/react-auth';
|
||||
import { useSolanaWallets } from '@privy-io/react-auth/solana';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import {
|
||||
getAssociatedTokenAddress,
|
||||
createTransferInstruction,
|
||||
TOKEN_PROGRAM_ID,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
} from "@solana/spl-token";
|
||||
|
||||
interface GpuPaymentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
gpu: {
|
||||
id: string;
|
||||
title: string;
|
||||
price_usd: number;
|
||||
price_per_hour: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CustomToken {
|
||||
address: string;
|
||||
id: number;
|
||||
project_name: 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 CUSTOM_TOKEN_API_URL = "https://catools.dev3vds1.link/get/vertex";
|
||||
|
||||
const USDC_MINT = new PublicKey("Es9vMFrzaCERJ8gLhEvX5yQceQ2uKcXfUrx2Wcikgqay");
|
||||
|
||||
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 top-0 left-0 bg-red-600 rounded-xl h-[54px] w-[54px]" />
|
||||
<svg
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 38 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="absolute top-2 left-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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();
|
||||
|
||||
const [solPrice, setSolPrice] = useState<number | null>(null);
|
||||
const [customToken, setCustomToken] = useState<CustomToken | null>(null);
|
||||
const [customTokenPrice, setCustomTokenPrice] = useState<number | null>(null);
|
||||
const [customTokenName, setCustomTokenName] = useState<string>('VERTEX');
|
||||
const [emailInput, setEmailInput] = useState('');
|
||||
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' | 'VERTEX'>('SOL');
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
// Calculate token amounts based on selected token
|
||||
const [currentTokenAmount, setCurrentTokenAmount] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSolPrice = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd'
|
||||
);
|
||||
setSolPrice(data.solana.usd);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch SOL price:", error);
|
||||
setErrorMsg('Failed to fetch SOL price.');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCustomToken = async () => {
|
||||
try {
|
||||
const { data } = await axios.get(CUSTOM_TOKEN_API_URL);
|
||||
if (data && Array.isArray(data) && data.length > 0) {
|
||||
setCustomToken(data[0]);
|
||||
|
||||
// Fetch the actual token price from CoinGecko or Jupiter Aggregator
|
||||
await fetchTokenPrice(data[0].address);
|
||||
|
||||
// Try to get token metadata using the address
|
||||
fetchTokenMetadata(data[0].address);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch custom token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTokenPrice = async (tokenAddress: string) => {
|
||||
try {
|
||||
// First attempt: Try using CoinGecko if the token is listed there by contract address
|
||||
// Convert Solana address to CoinGecko platform format
|
||||
const platformId = 'solana';
|
||||
const contractAddress = tokenAddress;
|
||||
|
||||
try {
|
||||
const coinGeckoUrl = `https://api.coingecko.com/api/v3/simple/token_price/${platformId}?contract_addresses=${contractAddress}&vs_currencies=usd`;
|
||||
const response = await axios.get(coinGeckoUrl);
|
||||
|
||||
// Check if we got a valid price
|
||||
if (response.data && response.data[contractAddress.toLowerCase()] && response.data[contractAddress.toLowerCase()].usd) {
|
||||
setCustomTokenPrice(response.data[contractAddress.toLowerCase()].usd);
|
||||
return;
|
||||
}
|
||||
} catch (cgError) {
|
||||
console.log("CoinGecko lookup failed, trying alternative source");
|
||||
}
|
||||
|
||||
// Second attempt: Try using Jupiter Aggregator API for Solana tokens
|
||||
try {
|
||||
// Jupiter provides price data for most Solana tokens
|
||||
const jupiterUrl = `https://price.jup.ag/v4/price?ids=${tokenAddress}`;
|
||||
const jupResponse = await axios.get(jupiterUrl);
|
||||
|
||||
if (jupResponse.data &&
|
||||
jupResponse.data.data &&
|
||||
jupResponse.data.data[tokenAddress] &&
|
||||
jupResponse.data.data[tokenAddress].price) {
|
||||
setCustomTokenPrice(jupResponse.data.data[tokenAddress].price);
|
||||
return;
|
||||
}
|
||||
} catch (jupError) {
|
||||
console.log("Jupiter API lookup failed, trying another alternative");
|
||||
}
|
||||
|
||||
// Third attempt: Try using Raydium API (another Solana DEX)
|
||||
try {
|
||||
const raydiumUrl = `https://api.raydium.io/v2/main/price?tokens=${tokenAddress}`;
|
||||
const raydiumResponse = await axios.get(raydiumUrl);
|
||||
|
||||
if (raydiumResponse.data && raydiumResponse.data[tokenAddress]) {
|
||||
setCustomTokenPrice(raydiumResponse.data[tokenAddress]);
|
||||
return;
|
||||
}
|
||||
} catch (raydiumError) {
|
||||
console.log("Raydium API lookup failed");
|
||||
}
|
||||
|
||||
// Fallback: If all API calls fail, use a fallback price or calculate from an existing pool
|
||||
console.warn("Could not fetch price from any API, using fallback price");
|
||||
setCustomTokenPrice(0.5); // Fallback price as last resort
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch token price:", error);
|
||||
// Default price as absolute fallback
|
||||
setCustomTokenPrice(0.5);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTokenMetadata = async (address: string) => {
|
||||
try {
|
||||
// First try to get metadata from Solana token registry
|
||||
const tokenRegistryUrl = `https://cdn.jsdelivr.net/gh/solana-labs/token-list@main/src/tokens/solana.tokenlist.json`;
|
||||
const registryResponse = await axios.get(tokenRegistryUrl);
|
||||
const tokenList = registryResponse.data.tokens;
|
||||
|
||||
// Find token in the registry by address
|
||||
const tokenInfo = tokenList.find((token: any) => token.address === address);
|
||||
|
||||
if (tokenInfo) {
|
||||
// Use token info from the registry
|
||||
setCustomTokenName(tokenInfo.symbol.toUpperCase());
|
||||
return;
|
||||
}
|
||||
|
||||
// If not found in registry, try to get on-chain metadata
|
||||
const tokenPublicKey = new PublicKey(address);
|
||||
const connection = new Connection(SOLANA_RPC);
|
||||
|
||||
// Attempt to get token account info
|
||||
const tokenMint = await connection.getTokenSupply(tokenPublicKey);
|
||||
|
||||
if (tokenMint) {
|
||||
// If we can't get a name from on-chain data, use project_name from our API
|
||||
// or fall back to default name
|
||||
if (customToken?.project_name) {
|
||||
setCustomTokenName(customToken.project_name.toUpperCase());
|
||||
} else {
|
||||
// If all else fails, use the default name
|
||||
setCustomTokenName('VERTEX');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If we couldn't get metadata, use the default name
|
||||
setCustomTokenName('VERTEX');
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch token metadata:", error);
|
||||
// Default to VERTEX if we can't get the name
|
||||
setCustomTokenName('VERTEX');
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
fetchSolPrice();
|
||||
fetchCustomToken();
|
||||
setErrorMsg(null);
|
||||
setSuccessMsg(null);
|
||||
setPaymentSuccess(false);
|
||||
setPaymentFailure(false);
|
||||
}
|
||||
}, [isOpen, customToken?.project_name]);
|
||||
|
||||
// Update token amount when price or selected token changes
|
||||
useEffect(() => {
|
||||
updateTokenAmount();
|
||||
}, [selectedToken, solPrice, customTokenPrice, gpu.price_usd]);
|
||||
|
||||
const updateTokenAmount = () => {
|
||||
if (selectedToken === 'SOL' && solPrice) {
|
||||
const amount = (gpu.price_usd / solPrice).toFixed(6);
|
||||
setCurrentTokenAmount(amount);
|
||||
} else if (selectedToken === 'USDC') {
|
||||
// USDC is a stablecoin, so 1 USDC ≈ 1 USD
|
||||
setCurrentTokenAmount(gpu.price_usd.toFixed(2));
|
||||
} else if (selectedToken === 'VERTEX' && customTokenPrice) {
|
||||
const amount = (gpu.price_usd / customTokenPrice).toFixed(2);
|
||||
setCurrentTokenAmount(amount);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayment = async () => {
|
||||
setLoading(true);
|
||||
setErrorMsg(null);
|
||||
setSuccessMsg(null);
|
||||
|
||||
try {
|
||||
if (!userEmail || !isValidEmail(userEmail)) {
|
||||
throw new Error("Please enter a valid email address for your receipt.");
|
||||
}
|
||||
|
||||
const solWallet = privyWallets[0];
|
||||
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 = "";
|
||||
|
||||
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`;
|
||||
|
||||
transaction.add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: fromPubKey,
|
||||
toPubkey: toPubKey,
|
||||
lamports: Math.floor(parseFloat(solAmount) * 1e9),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// For both USDC and VERTEX tokens
|
||||
const selectedMint = selectedToken === "USDC"
|
||||
? USDC_MINT
|
||||
: new PublicKey(customToken?.address || "");
|
||||
|
||||
const tokenDecimals = 6;
|
||||
|
||||
let tokenPrice = 1;
|
||||
if (selectedToken === "VERTEX") {
|
||||
tokenPrice = customTokenPrice || 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);
|
||||
|
||||
// Check if destination token account exists, if not create it
|
||||
const toTokenAccountInfo = await connection.getAccountInfo(toTokenAccount);
|
||||
|
||||
if (!toTokenAccountInfo) {
|
||||
// Import the necessary function
|
||||
const { createAssociatedTokenAccountInstruction } = await import("@solana/spl-token");
|
||||
|
||||
// Add instruction to create the associated token account for the business wallet
|
||||
transaction.add(
|
||||
createAssociatedTokenAccountInstruction(
|
||||
fromPubKey, // payer
|
||||
toTokenAccount, // associated token account address
|
||||
toPubKey, // owner of the new account
|
||||
selectedMint // token mint
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add the transfer instruction after ensuring the account exists
|
||||
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");
|
||||
|
||||
await axios.post(EMAIL_API_URL, {
|
||||
email: userEmail,
|
||||
product: gpu.title,
|
||||
price_hour: gpu.price_per_hour,
|
||||
price: gpu.price_usd.toFixed(2)
|
||||
});
|
||||
|
||||
// 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: `${gpu.id} ${gpu.title}`,
|
||||
user_email: userEmail,
|
||||
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}`);
|
||||
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
||||
setPaymentSuccess(true);
|
||||
} catch (err: any) {
|
||||
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;
|
||||
|
||||
// Show failure screen if payment failed
|
||||
if (paymentFailure) {
|
||||
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">
|
||||
<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]">(~{currentTokenAmount} {selectedToken})</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' | 'VERTEX')}
|
||||
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="VERTEX" className="bg-[#0E1618] text-[#0CE77E]">{customTokenName}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error and Success Messages */}
|
||||
{errorMsg && (
|
||||
<div className="p-2 text-sm text-red-400 border border-red-400 bg-red-400/10">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{successMsg && (
|
||||
<div className="p-2 text-sm text-[#0CE77E] border border-[#0CE77E] bg-[#0CE77E]/10">
|
||||
{successMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-between gap-3 mt-2">
|
||||
{/* Confirm Button */}
|
||||
<button
|
||||
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)]"
|
||||
>
|
||||
<CornerEdge position="left-top" />
|
||||
<CornerEdge position="right-top" />
|
||||
<CornerEdge position="left-bottom" />
|
||||
<CornerEdge position="right-bottom" />
|
||||
{loading ? 'PROCESSING...' : 'CONFIRM PAYMENT'}
|
||||
</button>
|
||||
|
||||
{/* Cancel Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
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)]"
|
||||
>
|
||||
<CornerEdge position="left-top" />
|
||||
<CornerEdge position="right-top" />
|
||||
<CornerEdge position="left-bottom" />
|
||||
<CornerEdge position="right-bottom" />
|
||||
CANCEL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GpuPaymentModal;
|
Loading…
x
Reference in New Issue
Block a user