'use client'; import { useState, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useMutation, useQuery } from '@tanstack/react-query'; 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, 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 = { 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); const isAdmin = user?.role === 'admin' || user?.role === 'technical'; const [step, setStep] = useState(0); const [form, setForm] = useState({ name: '', description: '', runtime: 'nodejs', databaseType: 'none', runtimeVersion: '20', phpVersion: '', dbVersion: '', gitUrl: '', gitToken: '', gitBranch: '', envVars: {}, cpuRequest: '100m', cpuLimit: '500m', memoryRequest: '128Mi', memoryLimit: '512Mi', replicas: 1, port: 3000, dbStorageSize: '1', appStorageSize: '2', enableRedis: false, enableRabbitmq: false, enableElasticsearch: false, }); const [envKey, setEnvKey] = useState(''); const [envVal, setEnvVal] = useState(''); const [sourceMethod, setSourceMethod] = useState<'git' | 'upload'>('upload'); const [zipFile, setZipFile] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [isDragging, setIsDragging] = useState(false); const [clusterMode, setClusterMode] = useState<'default' | 'manual' | 'pool'>('default'); const [showDbPassword, setShowDbPassword] = useState(false); const fileInputRef = useRef(null); const [dbDumpFile, setDbDumpFile] = useState(null); const dbDumpInputRef = useRef(null); const [dbUploadProgress, setDbUploadProgress] = useState(0); const [deployStage, setDeployStage] = useState('idle'); const [wpMode, setWpMode] = useState<'fresh' | 'migrate' | 'public_html'>('fresh'); const [wpContentFile, setWpContentFile] = useState(null); const [isWpDragging, setIsWpDragging] = useState(false); const wpFileInputRef = useRef(null); const [selectedCycle, setSelectedCycle] = useState('monthly'); const [paymentMethod, setPaymentMethod] = useState<'wallet' | 'gateway'>('wallet'); const [isPaid, setIsPaid] = useState(false); const { data: clusters = [] } = useQuery({ queryKey: ['clusters-public'], queryFn: () => api.get('/clusters/public').then((r) => r.data), enabled: isAdmin, }); const { data: pools = [] } = useQuery({ queryKey: ['pools-public'], queryFn: () => api.get('/clusters/pools/public').then((r) => r.data), enabled: isAdmin, }); // Cost calculation for the review step const { data: costData, isLoading: costLoading } = useQuery({ queryKey: ['deploy-cost', form.runtime, form.databaseType, form.cpuLimit, form.memoryLimit, form.replicas, form.dbStorageSize, form.appStorageSize, form.enableRedis, form.enableRabbitmq, form.enableElasticsearch], queryFn: () => api.post('/billing/calculate', { runtime: form.runtime, databaseType: form.databaseType, cpuLimit: form.cpuLimit, memoryLimit: form.memoryLimit, replicas: form.replicas, dbStorageSize: form.databaseType !== 'none' ? `${parseInt(form.dbStorageSize || '1', 10) || 1}` : undefined, appStorageSize: `${parseInt(form.appStorageSize || '2', 10) || 2}`, enableRedis: form.enableRedis, enableRabbitmq: form.enableRabbitmq, enableElasticsearch: form.enableElasticsearch, }).then((r) => r.data), enabled: step === 3, }); // Wallet balance for the review step payment const { data: walletData } = useQuery<{ balance: number }>({ queryKey: ['wallet-balance'], queryFn: () => api.get('/billing/wallet').then((r) => r.data), enabled: step === 3, }); const payAmount = costData ? costData[selectedCycle] : 0; const walletBalance = walletData?.balance ?? 0; const hasEnoughBalance = walletBalance >= payAmount; 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`; } if (payload.appStorageSize) { payload.appStorageSize = `${parseInt(payload.appStorageSize, 10) || 2}Gi`; } const res = await api.post('/applications', payload); const appId = res.data.id; // 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, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); } // 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, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setDbUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); } // 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})`, callbackUrl: `${window.location.origin}/dashboard/deploy`, }); // In production, redirect to gw.gatewayUrl // For now, auto-verify (simulated) await api.post('/billing/gateway/verify', { trackingCode: gw.trackingCode, amount: payAmount, }); // Now create the app setDeployStage('creating'); const payload = { ...form }; if (payload.databaseType !== 'none' && payload.dbStorageSize) { payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`; } if (payload.appStorageSize) { payload.appStorageSize = `${parseInt(payload.appStorageSize, 10) || 2}Gi`; } const res = await api.post('/applications', payload); const appId = res.data.id; // 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, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); } // 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, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setDbUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); } // 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/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, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); } // 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, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setDbUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); } 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); }, }); const addEnvVar = () => { if (envKey.trim()) { setForm({ ...form, envVars: { ...form.envVars, [envKey]: envVal } }); setEnvKey(''); setEnvVal(''); } }; const removeEnvVar = (key: string) => { const updated = { ...form.envVars }; delete updated[key]; setForm({ ...form, envVars: updated }); }; const handleSubmit = () => { const payload = { ...form }; // Format dbStorageSize with Gi suffix if (payload.databaseType !== 'none' && payload.dbStorageSize) { payload.dbStorageSize = `${parseInt(payload.dbStorageSize, 10) || 1}Gi`; } // Format appStorageSize with Gi suffix (all app types) if (payload.appStorageSize) { payload.appStorageSize = `${parseInt(payload.appStorageSize, 10) || 2}Gi`; } createMutation.mutate(payload); }; const handleFileSelect = useCallback((file: File) => { const validTypes = ['application/zip', 'application/x-zip-compressed', 'application/gzip', 'application/x-tar']; const validExtensions = ['.zip', '.tar.gz', '.tgz']; const hasValidExt = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext)); if (!hasValidExt && !validTypes.includes(file.type)) { toast.error('Only .zip or .tar.gz files are allowed'); return; } if (file.size > 100 * 1024 * 1024) { toast.error('File size must be less than 100MB'); return; } setZipFile(file); toast.success(`Selected: ${file.name}`); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) handleFileSelect(file); }, [handleFileSelect]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleWpFileSelect = useCallback((file: File) => { const validExtensions = ['.zip', '.tar.gz', '.tgz']; const hasValidExt = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext)); if (!hasValidExt) { toast.error('Only .zip or .tar.gz files are allowed'); return; } if (file.size > 200 * 1024 * 1024) { toast.error('WordPress files must be less than 200MB'); return; } setWpContentFile(file); // Auto-suggest app storage size based on file size (add 50% buffer, minimum 2GB) const fileSizeGb = file.size / (1024 * 1024 * 1024); const suggestedSize = Math.max(2, Math.ceil(fileSizeGb * 1.5)); setForm((prev) => ({ ...prev, appStorageSize: String(suggestedSize) })); toast.success(`WordPress files selected: ${file.name}`); }, []); const handleWpDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsWpDragging(false); const file = e.dataTransfer.files[0]; if (file) handleWpFileSelect(file); }, [handleWpFileSelect]); const canNext = () => { if (step === 0) { if (form.name.length < 2) return false; // WordPress: migrate or public_html mode requires wp-content file if (form.runtime === 'wordpress') { if ((wpMode === 'migrate' || wpMode === 'public_html') && !wpContentFile) return false; } else { if (sourceMethod === 'upload' && !zipFile) return false; if (sourceMethod === 'git' && !form.gitUrl) return false; } return true; } return true; }; return (

Deploy New Application

Follow the steps to deploy your app to the cloud.

{/* Step indicator */}
{steps.map((label, i) => (
{i < step ? '✓' : i + 1}
{i < steps.length - 1 && (
)}
))}
{/* Step 0: Basic Info */} {step === 0 && (

Basic Information

setForm({ ...form, name: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '-') })} />

Lowercase letters, numbers, and hyphens only