new payment system
This commit is contained in:
parent
e56a469cfa
commit
a2af10606e
11
package.json
11
package.json
@ -11,10 +11,20 @@
|
||||
"dependencies": {
|
||||
"@geist-ui/core": "^2.3.8",
|
||||
"@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/wallet-adapter-wallets": "^0.19.36",
|
||||
"@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",
|
||||
@ -24,6 +34,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8.0.0",
|
||||
|
70
src/app/dashboard/api/orders/route.ts
Normal file
70
src/app/dashboard/api/orders/route.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
);
|
||||
|
||||
const emailUser = process.env.EMAIL_USER!;
|
||||
const emailPass = process.env.EMAIL_PASS!;
|
||||
const businessEmail = process.env.BUSINESS_EMAIL!;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { gpuId, userEmail, txSignature, amountSol, status } = await req.json();
|
||||
|
||||
if (!gpuId || !userEmail || !amountSol || !status) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!['pending', 'success', 'failed'].includes(status)) {
|
||||
return NextResponse.json({ error: 'Invalid status value' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('orders').insert({
|
||||
gpu_id: gpuId,
|
||||
user_email: userEmail,
|
||||
amount_sol: amountSol,
|
||||
sol_tx_signature: txSignature || null,
|
||||
status,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('[Supabase error]', error);
|
||||
return NextResponse.json({ error: 'Database error' }, { status: 500 });
|
||||
}
|
||||
|
||||
// Send confirmation email
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: emailUser,
|
||||
pass: emailPass,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"GPU Store" <${emailUser}>`,
|
||||
to: userEmail,
|
||||
subject: `GPU Payment ${status === 'success' ? 'Confirmed' : 'Attempted'}`,
|
||||
html: `
|
||||
<p>Hi,</p>
|
||||
<p>Your order for GPU ID <strong>${gpuId}</strong> has status: <strong>${status}</strong>.</p>
|
||||
<p>Amount: ${amountSol} SOL</p>
|
||||
${
|
||||
txSignature
|
||||
? `<p>Transaction: <a href="https://solscan.io/tx/${txSignature}" target="_blank">${txSignature}</a></p>`
|
||||
: '<p>No transaction was recorded.</p>'
|
||||
}
|
||||
<p>Thank you for using our platform.</p>
|
||||
`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: 'Order recorded and email sent' });
|
||||
} catch (err: any) {
|
||||
console.error('[Order API Error]', err);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
@ -1,234 +1,185 @@
|
||||
"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: 'a4000',
|
||||
title: 'NVIDIA RTX A4000',
|
||||
subTitle: '16GB GDDR6 - 6144 CUDA Cores',
|
||||
price: '$0.20/hour ($147/mo)',
|
||||
price_usd: 147,
|
||||
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.26/hr ($190/month)',
|
||||
price_usd: 190,
|
||||
},
|
||||
{
|
||||
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: '4x4090',
|
||||
title: '4x NVIDIA RTX 4090',
|
||||
subTitle: '24 GB GDDR6X - 16384 GPU CUDA Cores',
|
||||
price: '$0.37/hr ($268/month)',
|
||||
price_usd: 268,
|
||||
},
|
||||
{
|
||||
title: "NVIDIA V100",
|
||||
subTitle: "32 GB HBM2 - 5120 CUDA Cores",
|
||||
price: "$0.44/hr ($317/month)",
|
||||
link: "https://nowpayments.io/payment?iid=5437270588",
|
||||
id: 'v100',
|
||||
title: 'NVIDIA V100',
|
||||
subTitle: '32 GB HBM2 - 5120 CUDA Cores',
|
||||
price: '$0.44/hr ($317/month)',
|
||||
price_usd: 317,
|
||||
},
|
||||
{
|
||||
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: 'l4ada',
|
||||
title: 'NVIDIA L4 Ada',
|
||||
subTitle: '24 GB GDDR6 - 7680 CUDA Cores',
|
||||
price: '$0.52/hr ($372/month)',
|
||||
price_usd: 372,
|
||||
},
|
||||
{
|
||||
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: 'a6000',
|
||||
title: 'NVIDIA RTX A6000',
|
||||
subTitle: '48 GB GDDR6 - 10752 CUDA Cores',
|
||||
price: '$0.53/hr ($380/month)',
|
||||
price_usd: 380,
|
||||
},
|
||||
{
|
||||
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: 'l40sada',
|
||||
title: 'NVIDIA L40S Ada',
|
||||
subTitle: '48 GB GDDR6 - 18176 CUDA Cores',
|
||||
price: '$1.24/hr ($890/month)',
|
||||
price_usd: 890,
|
||||
},
|
||||
{
|
||||
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: 'a100',
|
||||
title: 'NVIDIA A100 PCIe',
|
||||
subTitle: '40 GB HBM2 - 6912 CUDA Cores',
|
||||
price: '$1.62/hr ($1,166/month)',
|
||||
price_usd: 1166,
|
||||
},
|
||||
{
|
||||
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: 'h100nvl',
|
||||
title: 'NVIDIA H100 NVL',
|
||||
subTitle: '94 GB HBM3 - 14592 CUDA Cores',
|
||||
price: '$3.35/hr ($2,410/month)',
|
||||
price_usd: 2410,
|
||||
},
|
||||
{
|
||||
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: 'h100sxm',
|
||||
title: 'NVIDIA H100 SXM',
|
||||
subTitle: '80 GB HBM3 - 14592 CUDA Cores',
|
||||
price: '$3.60/hr ($2,592/month)',
|
||||
price_usd: 2592,
|
||||
},
|
||||
];
|
||||
|
||||
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`}
|
||||
>
|
||||
<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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
163
src/components/GpuPaymentModal.tsx
Normal file
163
src/components/GpuPaymentModal.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
Transaction,
|
||||
SystemProgram,
|
||||
} from '@solana/web3.js';
|
||||
import axios from 'axios';
|
||||
import { usePrivy } from '@privy-io/react-auth';
|
||||
import {
|
||||
useSolanaWallets,
|
||||
useSignTransaction,
|
||||
} from '@privy-io/react-auth/solana';
|
||||
|
||||
interface GpuPaymentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
gpu: {
|
||||
id: string;
|
||||
title: string;
|
||||
price_usd: number;
|
||||
};
|
||||
}
|
||||
|
||||
const SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
|
||||
const BUSINESS_WALLET = 'Y93ednSpre2XRjPTBafHU1BXPXKMPhujcYAEshS5pXm8K'; // <-- Replace this
|
||||
|
||||
export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => {
|
||||
const { user } = usePrivy();
|
||||
const { wallets } = useSolanaWallets();
|
||||
const { signTransaction } = useSignTransaction();
|
||||
|
||||
const [solPrice, setSolPrice] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
|
||||
const connection = new Connection(SOLANA_RPC);
|
||||
|
||||
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 {
|
||||
setErrorMsg('Failed to fetch SOL price.');
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
fetchSolPrice();
|
||||
setErrorMsg(null);
|
||||
setSuccessMsg(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handlePayment = async () => {
|
||||
setLoading(true);
|
||||
setErrorMsg(null);
|
||||
setSuccessMsg(null);
|
||||
|
||||
try {
|
||||
const solWallet = wallets[0];
|
||||
if (!solWallet || !solWallet.address) {
|
||||
throw new Error('No connected Solana wallet found.');
|
||||
}
|
||||
|
||||
const fromPubKey = new PublicKey(solWallet.address);
|
||||
const toPubKey = new PublicKey(BUSINESS_WALLET);
|
||||
const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6);
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: fromPubKey,
|
||||
toPubkey: toPubKey,
|
||||
lamports: Math.floor(parseFloat(solAmount) * 1e9),
|
||||
})
|
||||
);
|
||||
|
||||
transaction.feePayer = fromPubKey;
|
||||
const { blockhash } = await connection.getRecentBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
|
||||
const signedTx = await signTransaction({
|
||||
transaction,
|
||||
connection,
|
||||
address: solWallet.address,
|
||||
});
|
||||
|
||||
const txId = await connection.sendRawTransaction(signedTx.serialize());
|
||||
await connection.confirmTransaction(txId, 'confirmed');
|
||||
|
||||
await axios.post('/api/orders', {
|
||||
gpuId: gpu.id,
|
||||
userEmail: user?.email?.address,
|
||||
txSignature: txId,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
||||
} catch (err: any) {
|
||||
setErrorMsg(err.message || 'Payment failed.');
|
||||
await axios.post('/api/orders', {
|
||||
gpuId: gpu.id,
|
||||
userEmail: user?.email?.address,
|
||||
txSignature: null,
|
||||
status: 'failed',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...';
|
||||
|
||||
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>
|
||||
|
||||
{errorMsg && (
|
||||
<div className="mt-4 rounded-md bg-red-100 px-4 py-2 text-sm text-red-700">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{successMsg && (
|
||||
<div className="mt-4 rounded-md bg-green-100 px-4 py-2 text-sm text-green-700">
|
||||
{successMsg}
|
||||
</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}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default GpuPaymentModal;
|
@ -11,7 +11,7 @@ const solanaConnectors = toSolanaWalletConnectors({
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<PrivyProvider
|
||||
appId="cm5rifywi0397bjqma3ylfo1p"
|
||||
appId="cm9sh02lb00lblb0mmebf4xle"
|
||||
config={{
|
||||
appearance: {
|
||||
accentColor: "#0ce87e",
|
||||
|
Loading…
x
Reference in New Issue
Block a user