feat(frontend): lifecycle status, wallet balance, admin billing UI
- Wallet balance display in dashboard header - Lifecycle status badges (color-coded) in apps list - Plan expiry countdown column - Admin apps: suspended/pending-deletion summary cards - Admin billing: lifecycle settings management - Updated TypeScript types for lifecycle and billing
This commit is contained in:
@@ -7,10 +7,23 @@ import api from '@/lib/api';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { toast } from 'react-toastify';
|
||||
import type { CreateApplicationDto, ClusterPublic, ClusterPoolPublic, CostBreakdown, BillingCycle } from '@/types';
|
||||
import { Hexagon, FolderUp, Link as LinkIcon, CheckCircle, Package, Server, Scale, Home, Target, BarChart3, RotateCw, KeyRound, Rocket, Clock, Upload, XCircle, Database, Eye, EyeOff, RefreshCw, DollarSign, Wallet, CreditCard } from 'lucide-react';
|
||||
import { Hexagon, FolderUp, Link as LinkIcon, CheckCircle, Package, Server, Scale, Home, Target, BarChart3, RotateCw, KeyRound, Rocket, Clock, Upload, XCircle, Database, Eye, EyeOff, RefreshCw, DollarSign, Wallet, CreditCard, Loader2 } from 'lucide-react';
|
||||
|
||||
const steps = ['Basic Info', 'Versions & Database', 'Resources', 'Review'];
|
||||
|
||||
type DeployStage = 'idle' | 'creating' | 'uploading-source' | 'uploading-db' | 'paying' | 'deploying' | 'done' | 'error';
|
||||
|
||||
const stageLabels: Record<DeployStage, string> = {
|
||||
idle: '',
|
||||
creating: 'Creating application...',
|
||||
'uploading-source': 'Uploading source code...',
|
||||
'uploading-db': 'Uploading database dump...',
|
||||
paying: 'Processing payment...',
|
||||
deploying: 'Starting deployment...',
|
||||
done: 'Redirecting...',
|
||||
error: 'An error occurred',
|
||||
};
|
||||
|
||||
export default function DeployPage() {
|
||||
const router = useRouter();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
@@ -48,7 +61,8 @@ export default function DeployPage() {
|
||||
const [dbDumpFile, setDbDumpFile] = useState<File | null>(null);
|
||||
const dbDumpInputRef = useRef<HTMLInputElement>(null);
|
||||
const [dbUploadProgress, setDbUploadProgress] = useState(0);
|
||||
const [wpMode, setWpMode] = useState<'fresh' | 'migrate'>('fresh');
|
||||
const [deployStage, setDeployStage] = useState<DeployStage>('idle');
|
||||
const [wpMode, setWpMode] = useState<'fresh' | 'migrate' | 'public_html'>('fresh');
|
||||
const [wpContentFile, setWpContentFile] = useState<File | null>(null);
|
||||
const [isWpDragging, setIsWpDragging] = useState(false);
|
||||
const wpFileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -96,6 +110,7 @@ export default function DeployPage() {
|
||||
const walletPayMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// First create the app
|
||||
setDeployStage('creating');
|
||||
const payload = { ...form };
|
||||
if (payload.databaseType !== 'none' && payload.dbStorageSize) {
|
||||
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
|
||||
@@ -103,9 +118,11 @@ export default function DeployPage() {
|
||||
const res = await api.post('/applications', payload);
|
||||
const appId = res.data.id;
|
||||
|
||||
// Upload source (regular apps or WordPress migrate)
|
||||
const fileToUpload = form.runtime === 'wordpress' ? (wpMode === 'migrate' ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
|
||||
// Upload source (regular apps or WordPress migrate/public_html)
|
||||
const fileToUpload = form.runtime === 'wordpress' ? ((wpMode === 'migrate' || wpMode === 'public_html') ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
|
||||
if (fileToUpload) {
|
||||
setDeployStage('uploading-source');
|
||||
setUploadProgress(0);
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToUpload);
|
||||
await api.post(`/applications/${appId}/upload`, formData, {
|
||||
@@ -116,6 +133,8 @@ export default function DeployPage() {
|
||||
|
||||
// Upload DB dump
|
||||
if (form.databaseType !== 'none' && dbDumpFile) {
|
||||
setDeployStage('uploading-db');
|
||||
setDbUploadProgress(0);
|
||||
const formData = new FormData();
|
||||
formData.append('file', dbDumpFile);
|
||||
await api.post(`/applications/${appId}/db-upload`, formData, {
|
||||
@@ -125,24 +144,31 @@ export default function DeployPage() {
|
||||
}
|
||||
|
||||
// Deduct from wallet
|
||||
setDeployStage('paying');
|
||||
await api.post(`/billing/wallet/pay/${appId}`, { amount: payAmount, cycle: selectedCycle });
|
||||
|
||||
return res;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
setDeployStage('deploying');
|
||||
toast.success('Payment successful! Deploying...');
|
||||
api.post(`/deployments/applications/${res.data.id}/deploy`).catch(() => {});
|
||||
setDeployStage('done');
|
||||
router.push(`/dashboard/apps/${res.data.id}`);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setDeployStage('error');
|
||||
toast.error(err.response?.data?.message || 'Payment or deployment failed');
|
||||
setUploadProgress(0);
|
||||
setDbUploadProgress(0);
|
||||
setTimeout(() => setDeployStage('idle'), 2000);
|
||||
},
|
||||
});
|
||||
|
||||
const gatewayPayMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Initiate gateway
|
||||
setDeployStage('paying');
|
||||
const { data: gw } = await api.post('/billing/gateway/initiate', {
|
||||
amount: payAmount,
|
||||
description: `Deploy: ${form.name} (${selectedCycle})`,
|
||||
@@ -157,6 +183,7 @@ export default function DeployPage() {
|
||||
});
|
||||
|
||||
// Now create the app
|
||||
setDeployStage('creating');
|
||||
const payload = { ...form };
|
||||
if (payload.databaseType !== 'none' && payload.dbStorageSize) {
|
||||
payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`;
|
||||
@@ -164,9 +191,11 @@ export default function DeployPage() {
|
||||
const res = await api.post('/applications', payload);
|
||||
const appId = res.data.id;
|
||||
|
||||
// Upload source (regular apps or WordPress migrate)
|
||||
const fileToUpload = form.runtime === 'wordpress' ? (wpMode === 'migrate' ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
|
||||
// Upload source (regular apps or WordPress migrate/public_html)
|
||||
const fileToUpload = form.runtime === 'wordpress' ? ((wpMode === 'migrate' || wpMode === 'public_html') ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
|
||||
if (fileToUpload) {
|
||||
setDeployStage('uploading-source');
|
||||
setUploadProgress(0);
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToUpload);
|
||||
await api.post(`/applications/${appId}/upload`, formData, {
|
||||
@@ -177,6 +206,8 @@ export default function DeployPage() {
|
||||
|
||||
// Upload DB dump
|
||||
if (form.databaseType !== 'none' && dbDumpFile) {
|
||||
setDeployStage('uploading-db');
|
||||
setDbUploadProgress(0);
|
||||
const formData = new FormData();
|
||||
formData.append('file', dbDumpFile);
|
||||
await api.post(`/applications/${appId}/db-upload`, formData, {
|
||||
@@ -186,30 +217,39 @@ export default function DeployPage() {
|
||||
}
|
||||
|
||||
// Deduct from the wallet (which was just charged by gateway)
|
||||
setDeployStage('paying');
|
||||
await api.post(`/billing/wallet/pay/${appId}`, { amount: payAmount, cycle: selectedCycle });
|
||||
|
||||
return res;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
setDeployStage('deploying');
|
||||
toast.success('Payment successful! Deploying...');
|
||||
api.post(`/deployments/applications/${res.data.id}/deploy`).catch(() => {});
|
||||
setDeployStage('done');
|
||||
router.push(`/dashboard/apps/${res.data.id}`);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setDeployStage('error');
|
||||
toast.error(err.response?.data?.message || 'Payment failed');
|
||||
setUploadProgress(0);
|
||||
setDbUploadProgress(0);
|
||||
setTimeout(() => setDeployStage('idle'), 2000);
|
||||
},
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (data: CreateApplicationDto) => {
|
||||
setDeployStage('creating');
|
||||
const res = await api.post('/applications', data);
|
||||
const appId = res.data.id;
|
||||
|
||||
// Upload zip file if selected
|
||||
// Upload source (regular apps or WordPress migrate)
|
||||
const fileToUpload = data.runtime === 'wordpress' ? (wpMode === 'migrate' ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
|
||||
// Upload source (regular apps or WordPress migrate/public_html)
|
||||
const fileToUpload = data.runtime === 'wordpress' ? ((wpMode === 'migrate' || wpMode === 'public_html') ? wpContentFile : null) : (sourceMethod === 'upload' ? zipFile : null);
|
||||
if (fileToUpload) {
|
||||
setDeployStage('uploading-source');
|
||||
setUploadProgress(0);
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileToUpload);
|
||||
await api.post(`/applications/${appId}/upload`, formData, {
|
||||
@@ -222,6 +262,8 @@ export default function DeployPage() {
|
||||
|
||||
// Upload DB dump if provided and a database was requested
|
||||
if (data.databaseType && data.databaseType !== 'none' && dbDumpFile) {
|
||||
setDeployStage('uploading-db');
|
||||
setDbUploadProgress(0);
|
||||
const formData = new FormData();
|
||||
formData.append('file', dbDumpFile);
|
||||
await api.post(`/applications/${appId}/db-upload`, formData, {
|
||||
@@ -235,13 +277,18 @@ export default function DeployPage() {
|
||||
return res;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
setDeployStage('deploying');
|
||||
toast.success('Application created! Triggering deployment...');
|
||||
api.post(`/deployments/applications/${res.data.id}/deploy`).catch(() => {});
|
||||
setDeployStage('done');
|
||||
router.push(`/dashboard/apps/${res.data.id}`);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setDeployStage('error');
|
||||
toast.error(err.response?.data?.message || 'Failed to create application');
|
||||
setUploadProgress(0);
|
||||
setDbUploadProgress(0);
|
||||
setTimeout(() => setDeployStage('idle'), 2000);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -327,9 +374,9 @@ export default function DeployPage() {
|
||||
const canNext = () => {
|
||||
if (step === 0) {
|
||||
if (form.name.length < 2) return false;
|
||||
// WordPress: migrate mode requires wp-content file
|
||||
// WordPress: migrate or public_html mode requires wp-content file
|
||||
if (form.runtime === 'wordpress') {
|
||||
if (wpMode === 'migrate' && !wpContentFile) return false;
|
||||
if ((wpMode === 'migrate' || wpMode === 'public_html') && !wpContentFile) return false;
|
||||
} else {
|
||||
if (sourceMethod === 'upload' && !zipFile) return false;
|
||||
if (sourceMethod === 'git' && !form.gitUrl) return false;
|
||||
@@ -572,7 +619,7 @@ export default function DeployPage() {
|
||||
{form.runtime === 'wordpress' && (
|
||||
<div className="space-y-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Deployment Mode</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setWpMode('fresh'); setWpContentFile(null); }}
|
||||
@@ -588,7 +635,7 @@ export default function DeployPage() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWpMode('migrate')}
|
||||
onClick={() => { setWpMode('migrate'); setWpContentFile(null); }}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-colors ${
|
||||
wpMode === 'migrate' ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
@@ -599,6 +646,19 @@ export default function DeployPage() {
|
||||
Upload your WordPress files (wp-content, themes, plugins) and optionally a DB dump.
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setWpMode('public_html'); setWpContentFile(null); }}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-colors ${
|
||||
wpMode === 'public_html' ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<FolderUp className="w-5 h-5 text-purple-600" />
|
||||
<p className="mt-2 font-semibold text-sm text-gray-900">Upload public_html</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Upload your entire public_html directory (full WordPress root) and deploy.
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{wpMode === 'fresh' && (
|
||||
@@ -690,6 +750,83 @@ export default function DeployPage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wpMode === 'public_html' && (
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 bg-purple-50 border border-purple-200 rounded-xl">
|
||||
<p className="text-xs text-gray-600">
|
||||
<strong>Upload a ZIP</strong> of your entire <code className="bg-purple-100 px-1 rounded">public_html</code> directory (the full WordPress root):
|
||||
</p>
|
||||
<ul className="text-xs text-gray-500 mt-1 ml-4 list-disc space-y-0.5">
|
||||
<li><code className="bg-purple-100 px-1 rounded">wp-admin/</code>, <code className="bg-purple-100 px-1 rounded">wp-includes/</code>, <code className="bg-purple-100 px-1 rounded">wp-content/</code></li>
|
||||
<li><code className="bg-purple-100 px-1 rounded">wp-config.php</code>, <code className="bg-purple-100 px-1 rounded">.htaccess</code>, and all root PHP files</li>
|
||||
</ul>
|
||||
<p className="text-xs text-gray-500 mt-1.5">
|
||||
The system auto-detects the full WordPress root and deploys it accordingly.
|
||||
You can also upload a SQL database dump in the next step to restore your data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{wpContentFile ? (
|
||||
<div className="flex items-center justify-between p-4 bg-green-50 border border-green-200 rounded-xl">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center text-green-600">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-800">{wpContentFile.name}</p>
|
||||
<p className="text-xs text-green-600">
|
||||
{(wpContentFile.size / (1024 * 1024)).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setWpContentFile(null);
|
||||
if (wpFileInputRef.current) wpFileInputRef.current.value = '';
|
||||
}}
|
||||
className="text-sm text-red-500 hover:text-red-700 font-medium"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onDrop={handleWpDrop}
|
||||
onDragOver={(e) => { e.preventDefault(); setIsWpDragging(true); }}
|
||||
onDragLeave={(e) => { e.preventDefault(); setIsWpDragging(false); }}
|
||||
onClick={() => wpFileInputRef.current?.click()}
|
||||
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
||||
isWpDragging
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-300 hover:border-primary-400 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<FolderUp className="w-8 h-8 mx-auto text-gray-400" />
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
Drag & drop your public_html ZIP here
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
ZIP with full WordPress root (<strong>wp-admin/</strong>, <strong>wp-content/</strong>, ...) • Max 200MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={wpFileInputRef}
|
||||
type="file"
|
||||
accept=".zip,.tar.gz,.tgz"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleWpFileSelect(file);
|
||||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1520,6 +1657,134 @@ export default function DeployPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deploy Progress Overlay */}
|
||||
{deployStage !== 'idle' && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 space-y-6">
|
||||
<div className="text-center">
|
||||
{deployStage === 'error' ? (
|
||||
<XCircle className="w-12 h-12 text-red-500 mx-auto mb-3" />
|
||||
) : deployStage === 'done' ? (
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
|
||||
) : (
|
||||
<Loader2 className="w-12 h-12 text-primary-600 mx-auto mb-3 animate-spin" />
|
||||
)}
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{deployStage === 'error' ? 'Deployment Failed' : deployStage === 'done' ? 'Success!' : 'Deploying Application'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{stageLabels[deployStage]}</p>
|
||||
</div>
|
||||
|
||||
{/* Stage Progress Steps */}
|
||||
<div className="space-y-3">
|
||||
{/* Creating */}
|
||||
<div className="flex items-center gap-3">
|
||||
{deployStage === 'creating' ? (
|
||||
<Loader2 className="w-5 h-5 text-primary-600 animate-spin shrink-0" />
|
||||
) : ['uploading-source', 'uploading-db', 'paying', 'deploying', 'done'].includes(deployStage) ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 shrink-0" />
|
||||
) : deployStage === 'error' ? (
|
||||
<XCircle className="w-5 h-5 text-red-400 shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full border-2 border-gray-200 shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm ${deployStage === 'creating' ? 'text-gray-900 font-medium' : 'text-gray-500'}`}>
|
||||
Creating application
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Uploading source (only if we have a file) */}
|
||||
{((form.runtime === 'wordpress' && (wpMode === 'migrate' || wpMode === 'public_html') && wpContentFile) || (form.runtime !== 'wordpress' && sourceMethod === 'upload' && zipFile)) && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{deployStage === 'uploading-source' ? (
|
||||
<Loader2 className="w-5 h-5 text-primary-600 animate-spin shrink-0" />
|
||||
) : ['uploading-db', 'paying', 'deploying', 'done'].includes(deployStage) ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full border-2 border-gray-200 shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm flex-1 ${deployStage === 'uploading-source' ? 'text-gray-900 font-medium' : 'text-gray-500'}`}>
|
||||
Uploading source code
|
||||
{deployStage === 'uploading-source' && uploadProgress > 0 && (
|
||||
<span className="text-primary-600 font-semibold ml-2">{uploadProgress}%</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{deployStage === 'uploading-source' && (
|
||||
<div className="ml-8 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary-500 rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${uploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Uploading DB dump (only if we have a dump) */}
|
||||
{dbDumpFile && form.databaseType !== 'none' && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{deployStage === 'uploading-db' ? (
|
||||
<Loader2 className="w-5 h-5 text-primary-600 animate-spin shrink-0" />
|
||||
) : ['paying', 'deploying', 'done'].includes(deployStage) ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full border-2 border-gray-200 shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm flex-1 ${deployStage === 'uploading-db' ? 'text-gray-900 font-medium' : 'text-gray-500'}`}>
|
||||
Uploading database dump
|
||||
{deployStage === 'uploading-db' && dbUploadProgress > 0 && (
|
||||
<span className="text-primary-600 font-semibold ml-2">{dbUploadProgress}%</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{deployStage === 'uploading-db' && (
|
||||
<div className="ml-8 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary-500 rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${dbUploadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment (only if cost > 0) */}
|
||||
{costData && costData.monthly > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
{deployStage === 'paying' ? (
|
||||
<Loader2 className="w-5 h-5 text-primary-600 animate-spin shrink-0" />
|
||||
) : ['deploying', 'done'].includes(deployStage) ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full border-2 border-gray-200 shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm ${deployStage === 'paying' ? 'text-gray-900 font-medium' : 'text-gray-500'}`}>
|
||||
Processing payment
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deploying */}
|
||||
<div className="flex items-center gap-3">
|
||||
{deployStage === 'deploying' ? (
|
||||
<Loader2 className="w-5 h-5 text-primary-600 animate-spin shrink-0" />
|
||||
) : deployStage === 'done' ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full border-2 border-gray-200 shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm ${deployStage === 'deploying' ? 'text-gray-900 font-medium' : 'text-gray-500'}`}>
|
||||
Starting deployment
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user