Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
416
frontend/src/app/cc/page.tsx
Normal file
416
frontend/src/app/cc/page.tsx
Normal file
@@ -0,0 +1,416 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
import SidebarLayout from '@/components/SidebarLayout';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
interface CCPackage {
|
||||
id: number;
|
||||
name: string;
|
||||
price_usd: number;
|
||||
cc_amount: number;
|
||||
bonus_cc: number;
|
||||
total_cc: number;
|
||||
discount_percent: number;
|
||||
recommendations: number;
|
||||
cars_per_cc: number;
|
||||
}
|
||||
|
||||
interface ChargeHistory {
|
||||
id: number;
|
||||
amount: number;
|
||||
currency: string;
|
||||
cc_amount: number;
|
||||
bonus_cc?: number;
|
||||
payment_method: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function CCPurchasePage() {
|
||||
const { language } = useTranslation();
|
||||
const { user, token, isLoading: authLoading } = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const [packages, setPackages] = useState<CCPackage[]>([]);
|
||||
const [ccBalance, setCcBalance] = useState<number>(0);
|
||||
const [chargeHistory, setChargeHistory] = useState<ChargeHistory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [purchasing, setPurchasing] = useState<number | null>(null);
|
||||
const [selectedPackage, setSelectedPackage] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return; // Wait for auth state to load
|
||||
if (!user) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
loadData();
|
||||
}, [user, router, authLoading]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const [packagesRes, balanceRes, historyRes] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/api/cc/packages`),
|
||||
fetch(`${API_BASE_URL}/api/cc/balance`, { headers }),
|
||||
fetch(`${API_BASE_URL}/api/cc/charge-history`, { headers }),
|
||||
]);
|
||||
|
||||
if (packagesRes.ok) {
|
||||
const pkgs = await packagesRes.json();
|
||||
setPackages(pkgs);
|
||||
if (pkgs.length > 0) {
|
||||
setSelectedPackage(pkgs[1]?.id || pkgs[0]?.id); // Default to Standard
|
||||
}
|
||||
}
|
||||
|
||||
if (balanceRes.ok) {
|
||||
const balance = await balanceRes.json();
|
||||
setCcBalance(balance.cc_balance || 0);
|
||||
}
|
||||
|
||||
if (historyRes.ok) {
|
||||
const history = await historyRes.json();
|
||||
setChargeHistory(history);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchase = async (packageId: number) => {
|
||||
if (!token) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setPurchasing(packageId);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/cc/create-checkout-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ package_id: packageId }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Redirect to Stripe Checkout
|
||||
window.location.href = data.checkout_url;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to create checkout session');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Purchase error:', error);
|
||||
alert('Failed to process payment');
|
||||
} finally {
|
||||
setPurchasing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualRequest = async (packageId: number) => {
|
||||
if (!token) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const note = prompt(
|
||||
language === 'ko'
|
||||
? '결제 메모를 입력하세요 (예: 몽골 파트너 계좌로 입금)'
|
||||
: 'Enter payment note (e.g., Paid via Mongolian partner account)'
|
||||
);
|
||||
|
||||
if (note === null) return; // User cancelled
|
||||
|
||||
setPurchasing(packageId);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/cc/manual-request`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ package_id: packageId, payment_note: note }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert(
|
||||
language === 'ko'
|
||||
? '요청이 제출되었습니다. 관리자 확인 후 CC가 충전됩니다.'
|
||||
: 'Request submitted. CC will be credited after admin verification.'
|
||||
);
|
||||
loadData();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.detail || 'Failed to submit request');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Manual request error:', error);
|
||||
alert('Failed to submit request');
|
||||
} finally {
|
||||
setPurchasing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return (
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs">
|
||||
{language === 'ko' ? '완료' : 'Completed'}
|
||||
</span>
|
||||
);
|
||||
case 'pending':
|
||||
return (
|
||||
<span className="px-2 py-1 bg-yellow-100 text-yellow-700 rounded-full text-xs">
|
||||
{language === 'ko' ? '대기' : 'Pending'}
|
||||
</span>
|
||||
);
|
||||
case 'cancelled':
|
||||
return (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
||||
{language === 'ko' ? '취소됨' : 'Cancelled'}
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarLayout groupKey="billing">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800">
|
||||
{language === 'ko' ? 'CC 충전' : 'Purchase CC'}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{language === 'ko'
|
||||
? 'CC를 충전하여 차량 추천 서비스를 이용하세요'
|
||||
: 'Purchase CC to use car recommendation services'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Balance Card */}
|
||||
<div className="bg-gradient-to-r from-primary-600 to-primary-700 rounded-xl p-6 text-white mb-8 shadow-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-primary-100">
|
||||
{language === 'ko' ? '현재 잔액' : 'Current Balance'}
|
||||
</p>
|
||||
<p className="text-4xl font-bold mt-2">{ccBalance.toFixed(1)} CC</p>
|
||||
<p className="text-primary-200 mt-2 text-sm">
|
||||
{language === 'ko'
|
||||
? `1 CC = ${packages[0]?.cars_per_cc || 3}대 차량 추천`
|
||||
: language === 'mn'
|
||||
? `1 CC = ${packages[0]?.cars_per_cc || 3} машины санал`
|
||||
: language === 'ru'
|
||||
? `1 CC = ${packages[0]?.cars_per_cc || 3} рекомендаций авто`
|
||||
: `1 CC = ${packages[0]?.cars_per_cc || 3} car recommendations`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-6xl opacity-50">💳</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Package Cards */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
{language === 'ko' ? '패키지 선택' : 'Select Package'}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{packages.map((pkg) => (
|
||||
<div
|
||||
key={pkg.id}
|
||||
onClick={() => setSelectedPackage(pkg.id)}
|
||||
className={`relative bg-white rounded-xl shadow-lg p-6 cursor-pointer transition-all ${
|
||||
selectedPackage === pkg.id
|
||||
? 'ring-2 ring-primary-500 transform scale-[1.02]'
|
||||
: 'hover:shadow-xl'
|
||||
}`}
|
||||
>
|
||||
{pkg.discount_percent > 0 && (
|
||||
<div className="absolute -top-3 -right-3 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-bold">
|
||||
{pkg.discount_percent}% OFF
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-800">{pkg.name}</h3>
|
||||
<p className="text-3xl font-bold text-primary-600 mt-2">${pkg.price_usd}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">CC</span>
|
||||
<span className="font-semibold">{pkg.cc_amount} CC</span>
|
||||
</div>
|
||||
{pkg.bonus_cc > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">
|
||||
{language === 'ko' ? '보너스' : 'Bonus'}
|
||||
</span>
|
||||
<span className="font-semibold text-green-600">+{pkg.bonus_cc} CC</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm border-t pt-3">
|
||||
<span className="text-gray-600">
|
||||
{language === 'ko' ? '총 CC' : 'Total CC'}
|
||||
</span>
|
||||
<span className="font-bold text-lg">{pkg.total_cc} CC</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-gray-500">
|
||||
<span>{language === 'ko' ? '추천 가능' : 'Recommendations'}</span>
|
||||
<span>
|
||||
{pkg.recommendations}
|
||||
{language === 'ko' ? '대' : language === 'mn' ? ' машин' : language === 'ru' ? ' авто' : ' cars'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePurchase(pkg.id);
|
||||
}}
|
||||
disabled={purchasing !== null}
|
||||
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 disabled:opacity-50 transition"
|
||||
>
|
||||
{purchasing === pkg.id ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
{language === 'ko' ? '처리 중...' : 'Processing...'}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
💳 {language === 'ko' ? '카드 결제' : 'Pay with Card'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleManualRequest(pkg.id);
|
||||
}}
|
||||
disabled={purchasing !== null}
|
||||
className="w-full py-2 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50 disabled:opacity-50 transition"
|
||||
>
|
||||
{language === 'ko' ? '수동 결제 요청' : 'Manual Payment Request'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Info */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-8">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl">ℹ️</span>
|
||||
<div>
|
||||
<p className="font-semibold text-blue-800 mb-1">
|
||||
{language === 'ko' ? '결제 안내' : 'Payment Information'}
|
||||
</p>
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li>
|
||||
•{' '}
|
||||
{language === 'ko'
|
||||
? '카드 결제: Visa, Mastercard 지원 (Stripe 보안 결제)'
|
||||
: 'Card Payment: Visa, Mastercard supported (Stripe secure payment)'}
|
||||
</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{language === 'ko'
|
||||
? '수동 결제: 러시아 사용자는 몽골 파트너 계좌로 입금 후 요청해주세요'
|
||||
: 'Manual Payment: Russian users can pay via Mongolian partner account'}
|
||||
</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{language === 'ko'
|
||||
? '결제 완료 후 즉시 CC가 충전됩니다'
|
||||
: 'CC will be credited immediately after payment'}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charge History */}
|
||||
<div className="bg-white rounded-xl shadow-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
{language === 'ko' ? '충전 내역' : 'Purchase History'}
|
||||
</h2>
|
||||
|
||||
{chargeHistory.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<div className="text-4xl mb-2">📋</div>
|
||||
<p>{language === 'ko' ? '충전 내역이 없습니다' : 'No purchase history'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
||||
{chargeHistory.map((item) => (
|
||||
<div key={item.id} className="p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold">
|
||||
${item.amount} {item.currency}
|
||||
</span>
|
||||
{getStatusBadge(item.status)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
<div>
|
||||
CC: +{item.cc_amount}
|
||||
{item.bonus_cc ? ` (+${item.bonus_cc} bonus)` : ''}
|
||||
</div>
|
||||
<div>
|
||||
{language === 'ko' ? '방법' : 'Method'}: {item.payment_method}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{item.created_at ? new Date(item.created_at).toLocaleString() : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
205
frontend/src/app/cc/success/page.tsx
Normal file
205
frontend/src/app/cc/success/page.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
interface CheckoutResult {
|
||||
status: string;
|
||||
cc_amount: number;
|
||||
bonus_cc: number;
|
||||
total_cc: number;
|
||||
cc_balance: number;
|
||||
}
|
||||
|
||||
function SuccessContent() {
|
||||
const { language } = useTranslation();
|
||||
const { token } = useAuthStore();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
const [result, setResult] = useState<CheckoutResult | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const sessionId = searchParams.get('session_id');
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) {
|
||||
setError('Invalid session');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
verifyPayment();
|
||||
}, [sessionId, token]);
|
||||
|
||||
const verifyPayment = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/api/cc/checkout-success?session_id=${sessionId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setResult(data);
|
||||
} else {
|
||||
const err = await response.json();
|
||||
setError(err.detail || 'Failed to verify payment');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verification error:', error);
|
||||
setError('Failed to verify payment');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-primary-500 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">
|
||||
{language === 'ko' ? '결제 확인 중...' : 'Verifying payment...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 max-w-md w-full text-center">
|
||||
<div className="text-6xl mb-4">❌</div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
{language === 'ko' ? '결제 확인 실패' : 'Payment Verification Failed'}
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">{error}</p>
|
||||
<Link
|
||||
href="/cc"
|
||||
className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition"
|
||||
>
|
||||
{language === 'ko' ? '다시 시도' : 'Try Again'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (result?.status === 'completed') {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 max-w-md w-full text-center">
|
||||
<div className="text-6xl mb-4">🎉</div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
{language === 'ko' ? '결제 완료!' : 'Payment Successful!'}
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{language === 'ko'
|
||||
? 'CC가 성공적으로 충전되었습니다'
|
||||
: 'CC has been successfully credited to your account'}
|
||||
</p>
|
||||
|
||||
<div className="bg-green-50 rounded-xl p-6 mb-6">
|
||||
<div className="text-sm text-green-600 mb-2">
|
||||
{language === 'ko' ? '충전된 CC' : 'CC Credited'}
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-green-700">
|
||||
+{result.total_cc} CC
|
||||
</div>
|
||||
{result.bonus_cc > 0 && (
|
||||
<div className="text-sm text-green-600 mt-1">
|
||||
({result.cc_amount} + {result.bonus_cc} bonus)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-4 mb-6">
|
||||
<div className="text-sm text-gray-500 mb-1">
|
||||
{language === 'ko' ? '현재 잔액' : 'Current Balance'}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-800">
|
||||
{result.cc_balance.toFixed(1)} CC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/find-my-car"
|
||||
className="block w-full px-6 py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition"
|
||||
>
|
||||
{language === 'ko' ? '차량 추천 받기' : 'Get Car Recommendations'}
|
||||
</Link>
|
||||
<Link
|
||||
href="/cc"
|
||||
className="block w-full px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-semibold hover:bg-gray-50 transition"
|
||||
>
|
||||
{language === 'ko' ? '더 충전하기' : 'Purchase More CC'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Pending or other status
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 max-w-md w-full text-center">
|
||||
<div className="text-6xl mb-4">⏳</div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">
|
||||
{language === 'ko' ? '결제 처리 중' : 'Payment Processing'}
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{language === 'ko'
|
||||
? '결제가 처리 중입니다. 잠시 후 다시 확인해주세요.'
|
||||
: 'Your payment is being processed. Please check back shortly.'}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => verifyPayment()}
|
||||
className="w-full px-6 py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 transition"
|
||||
>
|
||||
{language === 'ko' ? '다시 확인' : 'Check Again'}
|
||||
</button>
|
||||
<Link
|
||||
href="/cc"
|
||||
className="block w-full px-6 py-3 border border-gray-300 text-gray-700 rounded-lg font-semibold hover:bg-gray-50 transition"
|
||||
>
|
||||
{language === 'ko' ? 'CC 충전 페이지로' : 'Back to CC Purchase'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CCSuccessPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SuccessContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user