initial commit
This commit is contained in:
719
app/services/cloud-instance/page.tsx
Normal file
719
app/services/cloud-instance/page.tsx
Normal file
@@ -0,0 +1,719 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Cloud,
|
||||
Server,
|
||||
HardDrive,
|
||||
Shield,
|
||||
AlertCircle,
|
||||
Check,
|
||||
Cpu,
|
||||
MapPin,
|
||||
ExternalLink,
|
||||
Terminal,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { checkSufficientBalance, formatCurrency } from '@/lib/balance-service'
|
||||
|
||||
interface Plan {
|
||||
id: string
|
||||
name: string
|
||||
price: {
|
||||
daily: number
|
||||
monthly: number
|
||||
}
|
||||
}
|
||||
|
||||
interface OSImage {
|
||||
id: string
|
||||
name: string
|
||||
price?: {
|
||||
daily: number
|
||||
monthly: number
|
||||
}
|
||||
}
|
||||
|
||||
interface DataCenter {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface DeploymentResult {
|
||||
status: string
|
||||
cloudid?: string
|
||||
message?: string
|
||||
password?: string
|
||||
ipv4?: string
|
||||
server_id?: string
|
||||
error?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default function CloudInstancePage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
|
||||
const [billingCycle, setBillingCycle] = useState<'daily' | 'monthly'>('monthly')
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
hostname: '',
|
||||
planId: '',
|
||||
image: '',
|
||||
password: '',
|
||||
dcslug: '',
|
||||
enableBackup: false,
|
||||
enablePublicIp: true,
|
||||
})
|
||||
|
||||
// Available plans
|
||||
const plans: Plan[] = [
|
||||
{ id: '10027', name: '2 vCPU, 4GB RAM, 80GB SSD', price: { daily: 60, monthly: 1600 } },
|
||||
{ id: '10028', name: '4 vCPU, 8GB RAM, 160GB SSD', price: { daily: 80, monthly: 2200 } },
|
||||
{ id: '10029', name: '8 vCPU, 32GB RAM, 480GB SSD', price: { daily: 100, monthly: 2800 } },
|
||||
{ id: '10030', name: '16 vCPU, 64GB RAM, 960GB SSD', price: { daily: 120, monthly: 3200 } },
|
||||
]
|
||||
|
||||
// Available OS images
|
||||
const images: OSImage[] = [
|
||||
{ id: 'almalinux-9.2-x86_64', name: 'Alma Linux 9.2' },
|
||||
{ id: 'centos-7.9-x86_64', name: 'CentOS 7.9' },
|
||||
{ id: 'debian-12-x86_64', name: 'Debian 12' },
|
||||
{ id: 'ubuntu-22.04-x86_64', name: 'Ubuntu 22.04 LTS' },
|
||||
{ id: 'windows-2022', name: 'Windows Server 2022', price: { daily: 200, monthly: 5000 } },
|
||||
]
|
||||
|
||||
// Data centers
|
||||
const dataCenters: DataCenter[] = [
|
||||
{ id: 'inmumbaizone2', name: 'Mumbai, India' },
|
||||
{ id: 'inbangalore', name: 'Bangalore, India' },
|
||||
]
|
||||
|
||||
// Get user balance from auth context
|
||||
const userBalance = user?.balance || 0
|
||||
|
||||
// Calculate total price
|
||||
const calculatePrice = () => {
|
||||
const selectedPlan = plans.find((p) => p.id === formData.planId)
|
||||
const selectedImage = images.find((i) => i.id === formData.image)
|
||||
|
||||
if (!selectedPlan) return 0
|
||||
|
||||
let total = selectedPlan.price[billingCycle]
|
||||
|
||||
// Add Windows license cost if applicable
|
||||
if (selectedImage?.price) {
|
||||
total += selectedImage.price[billingCycle]
|
||||
}
|
||||
|
||||
// Add backup cost (20% of plan price)
|
||||
if (formData.enableBackup) {
|
||||
total += selectedPlan.price[billingCycle] * 0.2
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
const handleDeploy = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
setDeploymentResult(null)
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/cloud-instance')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (
|
||||
!formData.hostname ||
|
||||
!formData.planId ||
|
||||
!formData.image ||
|
||||
!formData.password ||
|
||||
!formData.dcslug
|
||||
) {
|
||||
setError('All fields are required')
|
||||
return
|
||||
}
|
||||
|
||||
// Check password strength
|
||||
if (formData.password.length < 8) {
|
||||
setError('Password must be at least 8 characters long')
|
||||
return
|
||||
}
|
||||
|
||||
// Check balance
|
||||
const totalPrice = calculatePrice()
|
||||
const balanceCheck = await checkSufficientBalance(totalPrice)
|
||||
|
||||
if (!balanceCheck.sufficient) {
|
||||
setError(
|
||||
balanceCheck.error ||
|
||||
`Insufficient balance. You need ${formatCurrency(totalPrice)} but only have ${formatCurrency(balanceCheck.currentBalance || 0)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/services/deploy-cloude', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hostname: formData.hostname,
|
||||
planid: formData.planId,
|
||||
image: formData.image,
|
||||
dclocation: formData.dcslug,
|
||||
password: formData.password,
|
||||
cycle: billingCycle,
|
||||
amount: totalPrice,
|
||||
backup: formData.enableBackup,
|
||||
publicip: formData.enablePublicIp,
|
||||
}),
|
||||
})
|
||||
|
||||
const result: DeploymentResult = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `Deployment failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
setDeploymentResult(result)
|
||||
|
||||
// Create success message with server details
|
||||
let successMessage = 'Cloud instance deployed successfully!'
|
||||
if (result.cloudid) {
|
||||
successMessage += ` Server ID: ${result.cloudid}`
|
||||
}
|
||||
if (result.ipv4) {
|
||||
successMessage += ` | IP: ${result.ipv4}`
|
||||
}
|
||||
|
||||
setSuccess(successMessage)
|
||||
|
||||
// Reset form after successful deployment
|
||||
setFormData({
|
||||
hostname: '',
|
||||
planId: '',
|
||||
image: '',
|
||||
password: '',
|
||||
dcslug: '',
|
||||
enableBackup: false,
|
||||
enablePublicIp: true,
|
||||
})
|
||||
} else {
|
||||
throw new Error(result.message || 'Deployment failed')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Deployment error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Failed to deploy cloud instance')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setDeploymentResult(null)
|
||||
setSuccess('')
|
||||
setError('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Cloud className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Deploy Cloud Instance</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
High-performance cloud servers with instant deployment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to deploy a cloud instance.{' '}
|
||||
<a
|
||||
href="/auth?redirect=/services/cloud-instance"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="mb-8 bg-green-50 border-green-200">
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-800">
|
||||
<div className="space-y-3">
|
||||
<div className="font-semibold">{success}</div>
|
||||
|
||||
{deploymentResult?.ipv4 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-green-700">
|
||||
Server IP Address
|
||||
</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Input
|
||||
className="font-mono text-sm bg-green-100 border-green-300"
|
||||
value={deploymentResult.ipv4}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(deploymentResult.ipv4!)
|
||||
// You can add a toast notification here
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-green-700">Server ID</Label>
|
||||
<p className="text-sm font-mono bg-green-100 p-2 rounded border border-green-300 mt-1">
|
||||
{deploymentResult.cloudid || deploymentResult.server_id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deploymentResult?.password && (
|
||||
<div className="mt-3">
|
||||
<Label className="text-sm font-medium text-green-700">Root Password</Label>
|
||||
<div className="flex items-center mt-1">
|
||||
<Input
|
||||
type="password"
|
||||
className="font-mono text-sm bg-green-100 border-green-300"
|
||||
value={deploymentResult.password}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(deploymentResult.password!)
|
||||
// You can add a toast notification here
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
Please change this password after first login
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deploymentResult?.message && (
|
||||
<div className="mt-3 p-3 bg-green-100 rounded-md">
|
||||
<p className="text-sm text-green-700">{deploymentResult.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 mt-4 pt-3 border-t border-green-200">
|
||||
<Button
|
||||
onClick={() => router.push('/dashboard/instances')}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Server className="w-4 h-4 mr-2" />
|
||||
View All Instances
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// SSH connection button - you can implement this
|
||||
if (deploymentResult?.ipv4) {
|
||||
const sshCommand = `ssh root@${deploymentResult.ipv4}`
|
||||
navigator.clipboard.writeText(sshCommand)
|
||||
// Show toast notification
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Terminal className="w-4 h-4 mr-2" />
|
||||
Copy SSH Command
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onClick={resetForm}>
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs text-green-600">
|
||||
<p>✅ Deployment successful! Your server is being provisioned.</p>
|
||||
{/* <p>📧 Login details will be sent to your email once ready.</p> */}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{deploymentResult ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Successful!</CardTitle>
|
||||
<CardDescription>
|
||||
Your cloud instance has been deployed successfully.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Server ID</Label>
|
||||
<p className="text-sm font-mono bg-muted p-2 rounded">
|
||||
{deploymentResult.server_id}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<p className="text-sm capitalize">{deploymentResult.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button onClick={() => router.push('/dashboard/instances')}>
|
||||
View Instances
|
||||
</Button>
|
||||
<Button variant="outline" onClick={resetForm}>
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<form onSubmit={handleDeploy}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Cycle</CardTitle>
|
||||
<CardDescription>Choose your preferred billing period</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={billingCycle}
|
||||
onValueChange={(v) => setBillingCycle(v as 'daily' | 'monthly')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="daily">Daily</TabsTrigger>
|
||||
<TabsTrigger value="monthly">Monthly (Save 20%)</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Server Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Server Configuration</CardTitle>
|
||||
<CardDescription>Configure your cloud instance specifications</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="hostname">Hostname</Label>
|
||||
<Input
|
||||
id="hostname"
|
||||
placeholder="my-server.example.com"
|
||||
value={formData.hostname}
|
||||
onChange={(e) => setFormData({ ...formData, hostname: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="plan">Server Plan</Label>
|
||||
<Select
|
||||
value={formData.planId}
|
||||
onValueChange={(value) => setFormData({ ...formData, planId: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{plans.map((plan) => (
|
||||
<SelectItem key={plan.id} value={plan.id}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{plan.name}</span>
|
||||
<span className="ml-4 text-primary">
|
||||
₹{plan.price[billingCycle]}/
|
||||
{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="image">Operating System</Label>
|
||||
<Select
|
||||
value={formData.image}
|
||||
onValueChange={(value) => setFormData({ ...formData, image: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{images.map((image) => (
|
||||
<SelectItem key={image.id} value={image.id}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{image.name}</span>
|
||||
{image.price && (
|
||||
<span className="ml-4 text-primary">
|
||||
+₹{image.price[billingCycle]}/
|
||||
{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="datacenter">Data Center</Label>
|
||||
<Select
|
||||
value={formData.dcslug}
|
||||
onValueChange={(value) => setFormData({ ...formData, dcslug: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select location" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dataCenters.map((dc) => (
|
||||
<SelectItem key={dc.id} value={dc.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4" />
|
||||
{dc.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">Root Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Strong password (min 8 characters)"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Minimum 8 characters required
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="backup"
|
||||
checked={formData.enableBackup}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData({ ...formData, enableBackup: checked as boolean })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="backup" className="cursor-pointer">
|
||||
Enable automatic backups (+20% of plan cost)
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="publicip"
|
||||
checked={formData.enablePublicIp}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData({ ...formData, enablePublicIp: checked as boolean })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="publicip" className="cursor-pointer">
|
||||
Enable public IP address
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pricing Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Base Plan</span>
|
||||
<span>
|
||||
₹{plans.find((p) => p.id === formData.planId)?.price[billingCycle] || 0}
|
||||
</span>
|
||||
</div>
|
||||
{images.find((i) => i.id === formData.image)?.price && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Windows License</span>
|
||||
<span>
|
||||
₹
|
||||
{images.find((i) => i.id === formData.image)?.price?.[billingCycle] ||
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{formData.enableBackup && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Backup Service</span>
|
||||
<span>
|
||||
₹
|
||||
{(plans.find((p) => p.id === formData.planId)?.price[billingCycle] ||
|
||||
0) * 0.2}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">
|
||||
₹{calculatePrice()}/{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
userBalance >= calculatePrice() ? 'text-green-600' : 'text-red-600'
|
||||
}
|
||||
>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Deploy Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Deploying...
|
||||
</>
|
||||
) : (
|
||||
'Deploy Cloud Instance'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
{!deploymentResult && (
|
||||
<div className="mt-16 grid md:grid-cols-3 gap-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Server className="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle>High Performance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enterprise-grade hardware with NVMe SSD storage
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Shield className="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle>Secure & Reliable</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
DDoS protection and 99.99% uptime SLA
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HardDrive className="w-8 h-8 text-primary mb-2" />
|
||||
<CardTitle>Instant Deployment</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your server ready in less than 60 seconds
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// jRIfuQspDJlWdgXxLmqVFSUthwBOkezibPCyoTvHYcEANraGKMnZ
|
||||
// {
|
||||
// "status": "success",
|
||||
// "cloudid": "1645972",
|
||||
// "message": "Cloud Server deploy in process and as soon it get ready to use system will send you login detail over the email.",
|
||||
// "password": "00000000",
|
||||
// "ipv4": "103.189.89.32"
|
||||
// }
|
||||
426
app/services/hosting-control-panel/page.tsx
Normal file
426
app/services/hosting-control-panel/page.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Server, Shield, HardDrive, Globe, Mail, AlertCircle, Check, Settings } from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
interface HostingPlan {
|
||||
id: string
|
||||
controlPanel: string
|
||||
billingCycle: string[]
|
||||
price: {
|
||||
monthly: number
|
||||
yearly: number
|
||||
}
|
||||
storage: string
|
||||
bandwidth: string
|
||||
domainsAllowed: string
|
||||
ssl: boolean
|
||||
support: string
|
||||
popular?: boolean
|
||||
}
|
||||
|
||||
export default function HostingControlPanelPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly')
|
||||
const [selectedPlan, setSelectedPlan] = useState('')
|
||||
const [domain, setDomain] = useState('')
|
||||
|
||||
// Available hosting plans
|
||||
const plans: HostingPlan[] = [
|
||||
{
|
||||
id: 'plan_001',
|
||||
controlPanel: 'cPanel',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 500, yearly: 5000 },
|
||||
storage: '100 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: 'Unlimited',
|
||||
ssl: true,
|
||||
support: '24/7 Priority Support',
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: 'plan_002',
|
||||
controlPanel: 'Plesk',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 450, yearly: 4500 },
|
||||
storage: '80 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: '50',
|
||||
ssl: true,
|
||||
support: '24/7 Support',
|
||||
},
|
||||
{
|
||||
id: 'plan_003',
|
||||
controlPanel: 'DirectAdmin',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 350, yearly: 3500 },
|
||||
storage: '60 GB SSD',
|
||||
bandwidth: '2TB/month',
|
||||
domainsAllowed: '25',
|
||||
ssl: true,
|
||||
support: 'Email & Chat',
|
||||
},
|
||||
{
|
||||
id: 'plan_004',
|
||||
controlPanel: 'HestiaCP',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 199, yearly: 1999 },
|
||||
storage: '50 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: 'Unlimited',
|
||||
ssl: true,
|
||||
support: 'Email & Chat',
|
||||
},
|
||||
{
|
||||
id: 'plan_005',
|
||||
controlPanel: 'Webmin',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 149, yearly: 1499 },
|
||||
storage: '40 GB SSD',
|
||||
bandwidth: 'Unlimited',
|
||||
domainsAllowed: '10',
|
||||
ssl: true,
|
||||
support: 'Email Only',
|
||||
},
|
||||
{
|
||||
id: 'plan_006',
|
||||
controlPanel: 'VestaCP',
|
||||
billingCycle: ['monthly', 'yearly'],
|
||||
price: { monthly: 129, yearly: 1299 },
|
||||
storage: '30 GB SSD',
|
||||
bandwidth: '500 GB/month',
|
||||
domainsAllowed: '5',
|
||||
ssl: true,
|
||||
support: 'Email Only',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock user balance
|
||||
const userBalance = 10000
|
||||
|
||||
const handlePurchase = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/hosting-control-panel')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (!domain || !selectedPlan) {
|
||||
setError('Please fill in all required fields')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate domain format
|
||||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/
|
||||
if (!domainRegex.test(domain)) {
|
||||
setError('Please enter a valid domain name')
|
||||
return
|
||||
}
|
||||
|
||||
// Get selected plan price
|
||||
const plan = plans.find(p => p.id === selectedPlan)
|
||||
if (!plan) {
|
||||
setError('Please select a valid plan')
|
||||
return
|
||||
}
|
||||
|
||||
const totalPrice = plan.price[billingCycle]
|
||||
|
||||
// Check balance
|
||||
if (userBalance < totalPrice) {
|
||||
setError(`Insufficient balance. You need ₹${totalPrice.toFixed(2)} but only have ₹${userBalance.toFixed(2)}`)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
alert('Hosting package purchased successfully! (This is a demo - no actual purchase)')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Settings className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Hosting Control Panels</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Professional web hosting with your preferred control panel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to purchase hosting.{' '}
|
||||
<a href="/auth?redirect=/services/hosting-control-panel" className="text-primary underline">
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handlePurchase}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Cycle</CardTitle>
|
||||
<CardDescription>Choose your preferred billing period</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={billingCycle} onValueChange={(v) => setBillingCycle(v as 'monthly' | 'yearly')}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||
<TabsTrigger value="yearly">Yearly (Save 17%)</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Available Plans */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6">Choose Your Control Panel</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => (
|
||||
<Card
|
||||
key={plan.id}
|
||||
className={`relative cursor-pointer transition-all ${
|
||||
selectedPlan === plan.id ? 'ring-2 ring-primary' : ''
|
||||
} ${plan.popular ? 'border-primary' : ''}`}
|
||||
onClick={() => setSelectedPlan(plan.id)}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||
<span className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-3 py-1 rounded-full text-xs font-semibold">
|
||||
Most Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">{plan.controlPanel}</CardTitle>
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
₹{plan.price[billingCycle]}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
/{billingCycle === 'monthly' ? 'month' : 'year'}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.storage}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.bandwidth} Bandwidth</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.domainsAllowed} Domains</span>
|
||||
</div>
|
||||
{plan.ssl && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm">Free SSL Certificate</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{plan.support}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioGroup value={selectedPlan}>
|
||||
<div className="flex items-center justify-center pt-2">
|
||||
<RadioGroupItem value={plan.id} id={plan.id} />
|
||||
<Label htmlFor={plan.id} className="ml-2 cursor-pointer">
|
||||
Select this plan
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Domain Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Domain Configuration</CardTitle>
|
||||
<CardDescription>Enter the primary domain for your hosting account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Label htmlFor="domain">Primary Domain</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
You can add more domains later from your control panel
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Order Summary */}
|
||||
{selectedPlan && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Control Panel</span>
|
||||
<span>{plans.find(p => p.id === selectedPlan)?.controlPanel}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Billing Cycle</span>
|
||||
<span className="capitalize">{billingCycle}</span>
|
||||
</div>
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">
|
||||
₹{plans.find(p => p.id === selectedPlan)?.price[billingCycle]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span className={userBalance >= (plans.find(p => p.id === selectedPlan)?.price[billingCycle] || 0) ? 'text-green-600' : 'text-red-600'}>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Purchase Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={loading || !user || !selectedPlan}
|
||||
>
|
||||
{loading ? 'Processing...' : 'Purchase Hosting Package'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="mt-16">
|
||||
<h3 className="text-2xl font-semibold mb-8 text-center">All Plans Include</h3>
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Check className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">One-Click Apps</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Install WordPress, Joomla, and more with one click
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Shield className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Free SSL</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Secure your website with free SSL certificates
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Daily Backups</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatic daily backups with easy restore
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Mail className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Email Accounts</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create professional email addresses
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
582
app/services/human-developer/page.tsx
Normal file
582
app/services/human-developer/page.tsx
Normal file
@@ -0,0 +1,582 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Clock,
|
||||
Calendar,
|
||||
CalendarDays,
|
||||
Users,
|
||||
Code,
|
||||
Briefcase,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Star,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import { formatCurrency, validateBalance } from '@/lib/balance-service'
|
||||
|
||||
interface DeveloperPlan {
|
||||
id: string
|
||||
name: string
|
||||
price: string
|
||||
value: number
|
||||
icon: React.ReactNode
|
||||
popular: boolean
|
||||
features: string[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export default function HumanDeveloperPage() {
|
||||
const router = useRouter()
|
||||
const { user, balance, refreshBalance } = useAuth()
|
||||
const { toast } = useToast()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
|
||||
// Form state
|
||||
const [selectedPlan, setSelectedPlan] = useState('')
|
||||
const [requirements, setRequirements] = useState('')
|
||||
const [contactInfo, setContactInfo] = useState({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
// Available developer plans
|
||||
const plans: DeveloperPlan[] = [
|
||||
{
|
||||
id: 'hourly',
|
||||
name: 'Hourly',
|
||||
price: '₹2,000',
|
||||
value: 2000,
|
||||
icon: <Clock className="w-5 h-5" />,
|
||||
popular: false,
|
||||
description: 'Perfect for quick fixes and consultations',
|
||||
features: [
|
||||
'Flexible hours',
|
||||
'Pay as you go',
|
||||
'Minimum 2 hours',
|
||||
'Quick fixes & bug fixes',
|
||||
'Technical consultation',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'daily',
|
||||
name: 'Daily',
|
||||
price: '₹5,000',
|
||||
value: 5000,
|
||||
icon: <Calendar className="w-5 h-5" />,
|
||||
popular: true,
|
||||
description: 'Best for short-term projects and rapid development',
|
||||
features: [
|
||||
'8 hours per day',
|
||||
'Priority support',
|
||||
'Daily progress reports',
|
||||
'Dedicated developer',
|
||||
'Progress tracking',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'monthly',
|
||||
name: 'Monthly',
|
||||
price: '₹1,00,000',
|
||||
value: 100000,
|
||||
icon: <CalendarDays className="w-5 h-5" />,
|
||||
popular: false,
|
||||
description: 'Complete solution for large projects',
|
||||
features: [
|
||||
'Full-time developer (160+ hours)',
|
||||
'Dedicated project manager',
|
||||
'Weekly sprint planning',
|
||||
'Complete project planning',
|
||||
'Priority feature development',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Technology expertise areas
|
||||
const techStack = [
|
||||
'React & Next.js',
|
||||
'Node.js',
|
||||
'Python',
|
||||
'PHP',
|
||||
'Laravel',
|
||||
'Vue.js',
|
||||
'Angular',
|
||||
'React Native',
|
||||
'Flutter',
|
||||
'WordPress',
|
||||
'Shopify',
|
||||
'Database Design',
|
||||
'API Development',
|
||||
'DevOps',
|
||||
'UI/UX Design',
|
||||
'Mobile Apps',
|
||||
'E-commerce',
|
||||
'CMS Development',
|
||||
]
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/human-developer')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (!selectedPlan) {
|
||||
setError('Please select a plan')
|
||||
return
|
||||
}
|
||||
|
||||
if (!requirements.trim()) {
|
||||
setError('Please describe your project requirements')
|
||||
return
|
||||
}
|
||||
|
||||
if (!contactInfo.name || !contactInfo.email) {
|
||||
setError('Please fill in all contact information')
|
||||
return
|
||||
}
|
||||
|
||||
const selectedPlanData = plans.find((p) => p.id === selectedPlan)
|
||||
if (!selectedPlanData) {
|
||||
setError('Invalid plan selected')
|
||||
return
|
||||
}
|
||||
|
||||
// Check balance before proceeding
|
||||
const minimumDeposit = selectedPlanData.value * 0.5
|
||||
if (!validateBalance(balance || 0, minimumDeposit)) {
|
||||
setError(
|
||||
`Insufficient balance. Minimum deposit required: ${formatCurrency(minimumDeposit)}, Available: ${formatCurrency(balance || 0)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/services/hire-developer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
planId: selectedPlan,
|
||||
planName: selectedPlanData.name,
|
||||
planPrice: selectedPlanData.value,
|
||||
requirements: requirements.trim(),
|
||||
contactInfo: {
|
||||
name: contactInfo.name.trim(),
|
||||
email: contactInfo.email.trim(),
|
||||
phone: contactInfo.phone?.trim() || undefined,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to submit request')
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(data.data.message)
|
||||
toast({
|
||||
title: 'Request Submitted Successfully!',
|
||||
description: `Deposit of ${formatCurrency(minimumDeposit)} has been deducted. Request ID: ${data.data.requestId}`,
|
||||
})
|
||||
|
||||
// Refresh user balance
|
||||
await refreshBalance()
|
||||
|
||||
// Reset form
|
||||
setSelectedPlan('')
|
||||
setRequirements('')
|
||||
setContactInfo({
|
||||
name: user?.name || '',
|
||||
email: user?.email || '',
|
||||
phone: '',
|
||||
})
|
||||
} else {
|
||||
throw new Error(data.error?.message || 'Failed to submit request')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Developer hire error:', err)
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to submit request'
|
||||
setError(errorMessage)
|
||||
toast({
|
||||
title: 'Request Failed',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Users className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Hire Expert Developers</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Get skilled developers for your projects. From quick fixes to full-scale applications.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to hire developers.{' '}
|
||||
<a
|
||||
href="/auth?redirect=/services/human-developer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert className="mb-8">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertDescription>{success}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-8">
|
||||
{/* Plan Selection */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold mb-6">Choose Your Plan</h2>
|
||||
<RadioGroup value={selectedPlan} onValueChange={setSelectedPlan}>
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.id} className="relative">
|
||||
<RadioGroupItem value={plan.id} id={plan.id} className="peer sr-only" />
|
||||
<Label
|
||||
htmlFor={plan.id}
|
||||
className={`block cursor-pointer transition-all rounded-xl hover:shadow-lg ${
|
||||
plan.popular ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
>
|
||||
<Card
|
||||
className={`h-full transition-all ${
|
||||
selectedPlan === plan.id
|
||||
? 'border-primary border-2 shadow-lg bg-primary/5 ring-2 ring-primary/20'
|
||||
: 'hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
{plan.popular && (
|
||||
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
|
||||
<Badge className="bg-gradient-to-r from-blue-500 to-purple-600">
|
||||
<Star className="w-3 h-3 mr-1" />
|
||||
Most Popular
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlan === plan.id && (
|
||||
<div className="absolute -top-3 left-4">
|
||||
<Badge className="bg-gradient-to-r from-blue-500 to-purple-600 text-white">
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Selected
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="relative">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-3 text-white">
|
||||
{plan.icon}
|
||||
</div>
|
||||
{/* {selectedPlan === plan.id && (
|
||||
<div className="absolute -top-1 -right-1 w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center border-2 border-white">
|
||||
<CheckCircle className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
<CardTitle className="text-xl">{plan.name}</CardTitle>
|
||||
<div className="text-3xl font-bold text-primary">
|
||||
{plan.price}
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
/{plan.id}
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription className="mt-2">{plan.description}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<ul className="space-y-2">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* Project Requirements */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Requirements</CardTitle>
|
||||
<CardDescription>
|
||||
Describe your project in detail to help us match you with the right developer
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="requirements">Project Description *</Label>
|
||||
<Textarea
|
||||
id="requirements"
|
||||
rows={6}
|
||||
placeholder="Please describe your project requirements, technology stack, timeline, and any specific needs..."
|
||||
value={requirements}
|
||||
onChange={(e) => setRequirements(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Technology Stack */}
|
||||
<div>
|
||||
<Label>Our Technology Expertise</Label>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{techStack.map((tech, idx) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contact Information</CardTitle>
|
||||
<CardDescription>We'll use this to get in touch with you</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Full Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={contactInfo.name}
|
||||
onChange={(e) => setContactInfo({ ...contactInfo, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">Email Address *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={contactInfo.email}
|
||||
onChange={(e) => setContactInfo({ ...contactInfo, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">Phone Number</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="+91 98765 43210"
|
||||
value={contactInfo.phone}
|
||||
onChange={(e) => setContactInfo({ ...contactInfo, phone: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Balance Check & Order Summary */}
|
||||
{selectedPlan && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Plan Selected</span>
|
||||
<span>{plans.find((p) => p.id === selectedPlan)?.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Rate</span>
|
||||
<span>{plans.find((p) => p.id === selectedPlan)?.price}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Minimum Deposit (50%)</span>
|
||||
<span className="text-orange-600">
|
||||
{formatCurrency(
|
||||
(plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
balance &&
|
||||
balance >= (plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}
|
||||
>
|
||||
{formatCurrency(balance || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Deposit Required</span>
|
||||
<span className="text-primary">
|
||||
{formatCurrency(
|
||||
(plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-muted/50 rounded-lg">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
* A 50% deposit is required to start. Final pricing will be discussed based
|
||||
on your specific requirements and project scope.
|
||||
</p>
|
||||
</div>
|
||||
{balance &&
|
||||
balance < (plans.find((p) => p.id === selectedPlan)?.value || 0) * 0.5 && (
|
||||
<Alert className="mt-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Insufficient balance. Please{' '}
|
||||
<a href="/balance" className="text-primary underline">
|
||||
add funds to your account
|
||||
</a>{' '}
|
||||
before proceeding.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? 'Submitting Request...' : 'Submit Request & Proceed to Payment'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Why Choose Us */}
|
||||
<div className="mt-16">
|
||||
<h3 className="text-2xl font-semibold mb-8 text-center">Why Choose Our Developers?</h3>
|
||||
<div className="grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Code className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Expert Skills</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
5+ years experience in modern tech stacks
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<CheckCircle className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Quality Assured</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Code reviews and testing included
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Clock className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">On-Time Delivery</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Meeting deadlines is our priority
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-primary/10 rounded-full mb-3">
|
||||
<Briefcase className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-semibold mb-2">Full Support</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Post-delivery support and maintenance
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
642
app/services/kubernetes/page.tsx
Normal file
642
app/services/kubernetes/page.tsx
Normal file
@@ -0,0 +1,642 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Container,
|
||||
Server,
|
||||
HardDrive,
|
||||
Shield,
|
||||
AlertCircle,
|
||||
Check,
|
||||
Plus,
|
||||
Minus,
|
||||
Cpu,
|
||||
GitBranch,
|
||||
Download,
|
||||
ExternalLink,
|
||||
} from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
|
||||
interface NodePool {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
count: number
|
||||
autoscale: boolean
|
||||
minNodes?: number
|
||||
maxNodes?: number
|
||||
}
|
||||
|
||||
interface ClusterSize {
|
||||
id: string
|
||||
name: string
|
||||
price: {
|
||||
daily: number
|
||||
monthly: number
|
||||
}
|
||||
}
|
||||
|
||||
interface DeploymentResult {
|
||||
status: string
|
||||
clusterId?: string
|
||||
message?: string
|
||||
id?: string
|
||||
error?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default function KubernetesPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const [deploymentResult, setDeploymentResult] = useState<DeploymentResult | null>(null)
|
||||
const [billingCycle, setBillingCycle] = useState<'daily' | 'monthly'>('monthly')
|
||||
|
||||
// Form state
|
||||
const [clusterName, setClusterName] = useState('')
|
||||
const [k8sVersion, setK8sVersion] = useState('1.30.0-utho')
|
||||
const [nodePools, setNodePools] = useState<NodePool[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'default-pool',
|
||||
size: '10215',
|
||||
count: 2,
|
||||
autoscale: false,
|
||||
minNodes: 2,
|
||||
maxNodes: 4,
|
||||
},
|
||||
])
|
||||
|
||||
// Available cluster sizes
|
||||
const clusterSizes: ClusterSize[] = [
|
||||
{ id: '10215', name: '2 vCPU, 4GB RAM', price: { daily: 200, monthly: 4000 } },
|
||||
{ id: '10216', name: '4 vCPU, 8GB RAM', price: { daily: 300, monthly: 7000 } },
|
||||
{ id: '10217', name: '8 vCPU, 16GB RAM', price: { daily: 500, monthly: 12000 } },
|
||||
{ id: '10218', name: '16 vCPU, 32GB RAM', price: { daily: 800, monthly: 20000 } },
|
||||
]
|
||||
|
||||
// Kubernetes versions
|
||||
const k8sVersions = ['1.30.0-utho', '1.29.0-utho', '1.28.0-utho']
|
||||
|
||||
// Mock user balance
|
||||
const userBalance = 15000
|
||||
|
||||
// Calculate total price
|
||||
const calculatePrice = () => {
|
||||
let total = 0
|
||||
nodePools.forEach((pool) => {
|
||||
const size = clusterSizes.find((s) => s.id === pool.size)
|
||||
if (size) {
|
||||
total += size.price[billingCycle] * pool.count
|
||||
}
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
// Add node pool
|
||||
const addNodePool = () => {
|
||||
const newPool: NodePool = {
|
||||
id: Date.now().toString(),
|
||||
name: `pool-${nodePools.length + 1}`,
|
||||
size: '10215',
|
||||
count: 1,
|
||||
autoscale: false,
|
||||
minNodes: 1,
|
||||
maxNodes: 3,
|
||||
}
|
||||
setNodePools([...nodePools, newPool])
|
||||
}
|
||||
|
||||
// Remove node pool
|
||||
const removeNodePool = (id: string) => {
|
||||
if (nodePools.length > 1) {
|
||||
setNodePools(nodePools.filter((pool) => pool.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
// Update node pool
|
||||
const updateNodePool = (id: string, field: keyof NodePool, value: any) => {
|
||||
setNodePools(nodePools.map((pool) => (pool.id === id ? { ...pool, [field]: value } : pool)))
|
||||
}
|
||||
|
||||
// Format node pools for API
|
||||
const formatNodePools = () => {
|
||||
return nodePools.map((pool) => ({
|
||||
name: pool.name,
|
||||
size: pool.size,
|
||||
count: pool.count,
|
||||
autoscale: pool.autoscale,
|
||||
min_nodes: pool.minNodes || pool.count,
|
||||
max_nodes: pool.maxNodes || pool.count + 2,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDeploy = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setSuccess('')
|
||||
setDeploymentResult(null)
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/kubernetes')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if (!clusterName) {
|
||||
setError('Cluster name is required')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate cluster name
|
||||
if (!/^[a-z0-9-]+$/.test(clusterName)) {
|
||||
setError('Cluster name can only contain lowercase letters, numbers, and hyphens')
|
||||
return
|
||||
}
|
||||
|
||||
// Check balance
|
||||
const totalPrice = calculatePrice()
|
||||
if (userBalance < totalPrice) {
|
||||
setError(
|
||||
`Insufficient balance. You need ₹${totalPrice.toFixed(2)} but only have ₹${userBalance.toFixed(2)}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/services/deploy-kubernetes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
cluster_label: clusterName,
|
||||
cluster_version: k8sVersion,
|
||||
nodepools: formatNodePools(),
|
||||
cycle: billingCycle,
|
||||
amount: totalPrice,
|
||||
}),
|
||||
})
|
||||
|
||||
const result: DeploymentResult = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || `Deployment failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
if (result.status === 'success') {
|
||||
setDeploymentResult(result)
|
||||
setSuccess(
|
||||
`Kubernetes cluster "${clusterName}" deployed successfully! Cluster ID: ${result.clusterId || result.id}`
|
||||
)
|
||||
|
||||
// Reset form after successful deployment
|
||||
setClusterName('')
|
||||
setNodePools([
|
||||
{
|
||||
id: '1',
|
||||
name: 'default-pool',
|
||||
size: '10215',
|
||||
count: 2,
|
||||
autoscale: false,
|
||||
minNodes: 2,
|
||||
maxNodes: 4,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
throw new Error(result.message || 'Deployment failed')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Deployment error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Failed to deploy Kubernetes cluster')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadKubeconfig = async (clusterId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/services/deploy-kubernetes?clusterId=${clusterId}`)
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = `kubeconfig-${clusterId}.yaml`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
setError(errorData.message || 'Failed to download kubeconfig')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Kubeconfig download error:', err)
|
||||
setError('Failed to download kubeconfig')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setDeploymentResult(null)
|
||||
setSuccess('')
|
||||
setError('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Container className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">Kubernetes Edge</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Managed Kubernetes clusters with auto-scaling and enterprise-grade security
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to deploy a Kubernetes cluster.{' '}
|
||||
<a href="/auth?redirect=/services/kubernetes" className="text-primary underline">
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{deploymentResult ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Deployment Successful!</CardTitle>
|
||||
<CardDescription>
|
||||
Your Kubernetes cluster has been deployed successfully.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Cluster ID</Label>
|
||||
<p className="text-sm font-mono bg-muted p-2 rounded">
|
||||
{deploymentResult.clusterId || deploymentResult.id}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<p className="text-sm capitalize">{deploymentResult.status}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
{deploymentResult.clusterId && (
|
||||
<Button onClick={() => downloadKubeconfig(deploymentResult.clusterId!)}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download Kubeconfig
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => router.push('/dashboard/clusters')}>
|
||||
View Clusters
|
||||
</Button>
|
||||
<Button variant="outline" onClick={resetForm}>
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<form onSubmit={handleDeploy}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Cycle</CardTitle>
|
||||
<CardDescription>Choose your preferred billing period</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={billingCycle}
|
||||
onValueChange={(v) => setBillingCycle(v as 'daily' | 'monthly')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="daily">Daily</TabsTrigger>
|
||||
<TabsTrigger value="monthly">Monthly (Save 25%)</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Cluster Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cluster Configuration</CardTitle>
|
||||
<CardDescription>Configure your Kubernetes cluster settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="clusterName">Cluster Name</Label>
|
||||
<Input
|
||||
id="clusterName"
|
||||
placeholder="my-k8s-cluster"
|
||||
value={clusterName}
|
||||
onChange={(e) => setClusterName(e.target.value.toLowerCase())}
|
||||
pattern="^[a-z0-9-]+$"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Only lowercase letters, numbers, and hyphens allowed
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="k8sVersion">Kubernetes Version</Label>
|
||||
<Select value={k8sVersion} onValueChange={setK8sVersion}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{k8sVersions.map((version) => (
|
||||
<SelectItem key={version} value={version}>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="w-4 h-4" />v{version.replace('-utho', '')}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-blue-50 rounded-md border border-blue-200">
|
||||
<p className="text-sm text-blue-700">
|
||||
<strong>Location:</strong> Mumbai, India (Fixed)
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
All Kubernetes clusters are deployed in our Mumbai datacenter for optimal
|
||||
performance.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Node Pools */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
Node Pools
|
||||
<Button type="button" size="sm" onClick={addNodePool} variant="outline">
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Add Pool
|
||||
</Button>
|
||||
</CardTitle>
|
||||
<CardDescription>Configure worker node pools for your cluster</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{nodePools.map((pool, index) => (
|
||||
<Card key={pool.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
value={pool.name}
|
||||
onChange={(e) => updateNodePool(pool.id, 'name', e.target.value)}
|
||||
className="max-w-xs"
|
||||
placeholder="Pool name"
|
||||
required
|
||||
/>
|
||||
{nodePools.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeNodePool(pool.id)}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Node Size</Label>
|
||||
<Select
|
||||
value={pool.size}
|
||||
onValueChange={(value) => updateNodePool(pool.id, 'size', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clusterSizes.map((size) => (
|
||||
<SelectItem key={size.id} value={size.id}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{size.name}</span>
|
||||
<span className="ml-4 text-primary text-sm">
|
||||
₹{size.price[billingCycle]}/
|
||||
{billingCycle === 'daily' ? 'day' : 'mo'}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Node Count</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
updateNodePool(pool.id, 'count', Math.max(1, pool.count - 1))
|
||||
}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
value={pool.count}
|
||||
onChange={(e) =>
|
||||
updateNodePool(pool.id, 'count', parseInt(e.target.value) || 1)
|
||||
}
|
||||
className="w-20 text-center"
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
updateNodePool(pool.id, 'count', Math.min(10, pool.count + 1))
|
||||
}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Pool cost: ₹
|
||||
{(clusterSizes.find((s) => s.id === pool.size)?.price[billingCycle] ||
|
||||
0) * pool.count}
|
||||
/{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pricing Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{nodePools.map((pool) => {
|
||||
const size = clusterSizes.find((s) => s.id === pool.size)
|
||||
const cost = (size?.price[billingCycle] || 0) * pool.count
|
||||
return (
|
||||
<div key={pool.id} className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{pool.name} ({pool.count} nodes × {size?.name})
|
||||
</span>
|
||||
<span>₹{cost}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">
|
||||
₹{calculatePrice()}/{billingCycle === 'daily' ? 'day' : 'month'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
userBalance >= calculatePrice() ? 'text-green-600' : 'text-red-600'
|
||||
}
|
||||
>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Deploy Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Deploying Cluster...
|
||||
</>
|
||||
) : (
|
||||
'Deploy Kubernetes Cluster'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
{!deploymentResult && (
|
||||
<div className="mt-16 grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Cpu className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Auto-scaling</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Automatically scale nodes based on workload
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Shield className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Secure by Default</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
RBAC, network policies, and encryption
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Server className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Load Balancing</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Built-in load balancers for your services
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<HardDrive className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Persistent Storage</h4>
|
||||
<p className="text-sm text-muted-foreground">SSD-backed persistent volumes</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
202
app/services/page.tsx
Normal file
202
app/services/page.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { CustomSolutionCTA } from '@/components/ui/custom-solution-cta'
|
||||
import {
|
||||
Server,
|
||||
Database,
|
||||
Mail,
|
||||
Shield,
|
||||
Container,
|
||||
Cog,
|
||||
ArrowRight,
|
||||
Check,
|
||||
Users,
|
||||
Cloud,
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ServicesPage() {
|
||||
// Simulating database fetch - in production this would be from API/database
|
||||
const services = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Cloud Instance',
|
||||
description: 'High-performance cloud servers with instant deployment and scalable resources',
|
||||
price: 'Starting at ₹200/day',
|
||||
imageUrl: '/assets/images/services/cloud-instance.jpg',
|
||||
features: ['Instant Deployment', 'Auto Scaling', 'Load Balancing', '99.99% Uptime SLA'],
|
||||
learnMoreUrl: '/services/cloud-instance',
|
||||
buyButtonUrl: '/services/cloud-instance',
|
||||
buyButtonText: 'Deploy Now',
|
||||
popular: false,
|
||||
icon: Cloud,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Kubernetes',
|
||||
description: 'Managed Kubernetes clusters with auto-scaling and enterprise-grade security',
|
||||
price: 'Starting at ₹4000/month',
|
||||
imageUrl: '/assets/images/services/kubernetes-edge.png',
|
||||
features: ['Auto-scaling', 'Load Balancing', 'CI/CD Ready', 'Built-in Monitoring'],
|
||||
learnMoreUrl: '/services/kubernetes',
|
||||
buyButtonUrl: '/services/kubernetes',
|
||||
buyButtonText: 'Create Cluster',
|
||||
popular: true,
|
||||
icon: Container,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Hosting Control Panel',
|
||||
description: 'Professional web hosting with cPanel, Plesk, or DirectAdmin',
|
||||
price: 'Starting at ₹500/month',
|
||||
imageUrl: '/assets/images/services/hosting-cp.jpg',
|
||||
features: ['cPanel/Plesk/DirectAdmin', 'One-Click Apps', 'SSL Certificates', 'Daily Backups'],
|
||||
learnMoreUrl: '/services/hosting-control-panel',
|
||||
buyButtonUrl: '/services/hosting-control-panel',
|
||||
buyButtonText: 'Get Started',
|
||||
popular: false,
|
||||
icon: Cog,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'VPN Services',
|
||||
description: 'Secure WireGuard VPN with QR code setup and multiple locations',
|
||||
price: 'Starting at ₹300/month',
|
||||
imageUrl: '/assets/images/services/wiregurd-thumb.jpg',
|
||||
features: ['WireGuard Protocol', 'QR Code Setup', 'Multiple Locations', 'No Logs Policy'],
|
||||
learnMoreUrl: '/services/vpn',
|
||||
buyButtonUrl: '/services/vpn',
|
||||
buyButtonText: 'Setup VPN',
|
||||
popular: false,
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Human Developer',
|
||||
description: 'Skilled developers for your custom projects and requirements',
|
||||
price: 'Custom Quote',
|
||||
imageUrl: '/assets/images/services/human-developer.jpg',
|
||||
features: ['Expert Developers', 'Custom Solutions', 'Project Management', '24/7 Support'],
|
||||
learnMoreUrl: '/services/human-developer',
|
||||
buyButtonUrl: '/services/human-developer',
|
||||
buyButtonText: 'Hire Developer',
|
||||
popular: false,
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'VPS Hosting',
|
||||
description: 'Virtual private servers with full root access and dedicated resources',
|
||||
price: 'Starting at ₹800/month',
|
||||
imageUrl: '/assets/images/services/vps_thumb.jpg',
|
||||
features: ['Full Root Access', 'SSD Storage', 'Multiple OS Options', 'IPv6 Support'],
|
||||
learnMoreUrl: '/services/vps',
|
||||
buyButtonUrl: '/services/vps',
|
||||
buyButtonText: 'Order VPS',
|
||||
popular: false,
|
||||
icon: Server,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold mb-4">Our Premium Services</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Tailored solutions to accelerate your business growth
|
||||
</p>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 to-purple-600 mx-auto mt-6"></div>
|
||||
</div>
|
||||
|
||||
{/* Services Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{services.map((service) => {
|
||||
const Icon = service.icon
|
||||
return (
|
||||
<Card
|
||||
key={service.id}
|
||||
className={`relative overflow-hidden hover:shadow-lg transition-shadow ${service.popular ? 'ring-2 ring-primary' : ''}`}
|
||||
>
|
||||
{/* Most Popular Badge */}
|
||||
{service.popular && (
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<span className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-3 py-1 rounded-full text-xs font-semibold">
|
||||
Most Popular
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Service Thumbnail */}
|
||||
<div className="h-48 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 flex items-center justify-center">
|
||||
{service.imageUrl ? (
|
||||
<div className="relative w-full h-full">
|
||||
{/* Using div with background for now since images aren't available */}
|
||||
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-900">
|
||||
<Icon className="w-16 h-16 text-primary opacity-20" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Icon className="w-16 h-16 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Service Content */}
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-xl mb-2">{service.title}</CardTitle>
|
||||
<div className="text-lg font-semibold text-primary mb-2">{service.price}</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground text-center">{service.description}</p>
|
||||
|
||||
{/* Features List */}
|
||||
<ul className="space-y-2">
|
||||
{service.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-muted-foreground">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
{service.learnMoreUrl && (
|
||||
<Link href={service.learnMoreUrl} className="flex-1">
|
||||
<Button variant="outline" className="w-full">
|
||||
Learn More
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{service.buyButtonUrl && service.buyButtonText && (
|
||||
<Link href={service.buyButtonUrl} className="flex-1">
|
||||
<Button className="w-full">
|
||||
{service.buyButtonText}
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Additional Services Note */}
|
||||
<CustomSolutionCTA
|
||||
description="We offer tailored solutions for enterprise clients with specific requirements"
|
||||
buttonText="Contact Sales"
|
||||
buttonHref="/contact"
|
||||
/>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
431
app/services/vpn/page.tsx
Normal file
431
app/services/vpn/page.tsx
Normal file
@@ -0,0 +1,431 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Shield, MapPin, Download, QrCode, AlertCircle, Lock, Globe, Zap, Eye } from 'lucide-react'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { env } from 'process'
|
||||
|
||||
interface VPNLocation {
|
||||
code: string
|
||||
name: string
|
||||
flag: string
|
||||
popular?: boolean
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export default function VPNPage() {
|
||||
const router = useRouter()
|
||||
const { user } = useAuth()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly')
|
||||
const [selectedLocation, setSelectedLocation] = useState('')
|
||||
const [showConfig, setShowConfig] = useState(false)
|
||||
const [apiEndpoint, setApiEndpoint] = useState('')
|
||||
const [mockConfig, setMockConfig] = useState()
|
||||
// VPN pricing
|
||||
const pricing = {
|
||||
monthly: 300,
|
||||
yearly: 4023, // Save 17%
|
||||
}
|
||||
|
||||
// Available VPN locations
|
||||
const locations: VPNLocation[] = [
|
||||
// { code: 'india', name: 'Mumbai, India', flag: '🇮🇳', popular: true },
|
||||
{
|
||||
code: 'america',
|
||||
name: 'America, USA',
|
||||
flag: '🇺🇸',
|
||||
popular: true,
|
||||
endpoint: 'https://wireguard-vpn.3027622.siliconpin.com/vpn',
|
||||
},
|
||||
{
|
||||
code: 'europe',
|
||||
name: 'Europe, UK',
|
||||
flag: '🇬🇧',
|
||||
popular: true,
|
||||
endpoint: 'https://wireguard.vps20.siliconpin.com/vpn',
|
||||
},
|
||||
// { code: 'singapore', name: 'Singapore', flag: '🇸🇬' },
|
||||
// { code: 'japan', name: 'Tokyo, Japan', flag: '🇯🇵' },
|
||||
// { code: 'canada', name: 'Toronto, Canada', flag: '🇨🇦' },
|
||||
]
|
||||
|
||||
// Mock user balance
|
||||
const userBalance = 8000
|
||||
|
||||
const handleSetupVPN = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!user) {
|
||||
router.push('/auth?redirect=/services/vpn')
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedLocation) {
|
||||
setError('Please select a VPN location')
|
||||
return
|
||||
}
|
||||
|
||||
const totalPrice = pricing[billingCycle]
|
||||
if (userBalance < totalPrice) {
|
||||
setError(`Insufficient balance. You need ₹${totalPrice} but only have ₹${userBalance}`)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// Generate a unique order ID (you might want to use a proper ID generation)
|
||||
const orderId = `vpn-${Date.now()}`
|
||||
// const orderId = `vpn-${Date.now()}-${user.id.slice(0, 8)}`
|
||||
|
||||
// Make API request to our backend
|
||||
const response = await fetch('/api/services/deploy-vpn', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
orderId,
|
||||
location: selectedLocation,
|
||||
plan: billingCycle,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to setup VPN service')
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
|
||||
// Set the config content and show the config section
|
||||
setMockConfig(data.config)
|
||||
setShowConfig(true)
|
||||
} catch (err) {
|
||||
setError(err.message || 'Failed to setup VPN service')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadConfig = () => {
|
||||
const blob = new Blob([mockConfig], { type: 'application/octet-stream' }) // force binary
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
// quote filename properly
|
||||
a.setAttribute('download', `siliconpin-vpn-${selectedLocation}.conf`)
|
||||
a.setAttribute('type', 'application/octet-stream')
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
console.log('selectedLocation', selectedLocation)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full mb-4">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4">WireGuard VPN</h1>
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Secure, fast, and private VPN service with WireGuard protocol
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<Alert className="mb-8">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You need to be logged in to setup VPN.{' '}
|
||||
<a href="/auth?redirect=/services/vpn" className="text-primary underline">
|
||||
Click here to login
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!showConfig ? (
|
||||
<form onSubmit={handleSetupVPN}>
|
||||
<div className="space-y-8">
|
||||
{/* Billing Cycle */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Plan</CardTitle>
|
||||
<CardDescription>Choose your preferred billing cycle</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={billingCycle}
|
||||
onValueChange={(v) => setBillingCycle(v as 'monthly' | 'yearly')}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="monthly">Monthly - ₹{pricing.monthly}</TabsTrigger>
|
||||
<TabsTrigger value="yearly">
|
||||
Yearly - ₹{pricing.yearly} (Save 17%)
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Location Selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Select VPN Location</CardTitle>
|
||||
<CardDescription>
|
||||
Choose the server location for your VPN connection
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RadioGroup value={selectedLocation} onValueChange={setSelectedLocation}>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{locations.map((location) => (
|
||||
<div key={location.code} className="relative">
|
||||
<RadioGroupItem
|
||||
value={location.code}
|
||||
id={location.code}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={location.code}
|
||||
className={`flex items-center justify-between p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-primary/50 peer-checked:border-primary peer-checked:bg-primary/5 ${
|
||||
location.popular ? 'border-primary/30' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">{location.flag}</span>
|
||||
<div>
|
||||
<div className="font-medium">{location.name}</div>
|
||||
{location.popular && (
|
||||
<div className="text-xs text-primary font-medium">
|
||||
Most Popular
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<MapPin className="w-4 h-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Order Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">WireGuard VPN</span>
|
||||
<span>₹{pricing[billingCycle]}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Billing Cycle</span>
|
||||
<span className="capitalize">{billingCycle}</span>
|
||||
</div>
|
||||
<div className="border-t pt-2">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span className="text-primary">₹{pricing[billingCycle]}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">Your Balance</span>
|
||||
<span
|
||||
className={
|
||||
userBalance >= pricing[billingCycle]
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}
|
||||
>
|
||||
₹{userBalance.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Setup Button */}
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading || !user}>
|
||||
{loading ? 'Setting up VPN...' : 'Setup VPN Service'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
/* VPN Configuration */
|
||||
<div className="space-y-8">
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Your VPN service has been activated! Download the configuration file below.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>VPN Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Use this configuration with any WireGuard client
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Config File */}
|
||||
<div>
|
||||
<Label>Configuration File</Label>
|
||||
<div className="mt-2 p-4 bg-muted rounded-lg">
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap text-muted-foreground">
|
||||
{mockConfig}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<Button onClick={downloadConfig} className="flex-1">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download Config File
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => alert('QR Code functionality would be implemented here')}
|
||||
>
|
||||
<QrCode className="w-4 h-4 mr-2" />
|
||||
Show QR Code
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Reset */}
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowConfig(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Setup New VPN
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Setup Instructions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Setup Instructions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">For Desktop/Laptop:</h4>
|
||||
<ol className="text-sm space-y-1 list-decimal list-inside text-muted-foreground">
|
||||
<li>Download and install WireGuard client from wireguard.com</li>
|
||||
<li>Import the downloaded configuration file</li>
|
||||
<li>Click "Activate" to connect to the VPN</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium">For Mobile:</h4>
|
||||
<ol className="text-sm space-y-1 list-decimal list-inside text-muted-foreground">
|
||||
<li>Install WireGuard app from App Store/Play Store</li>
|
||||
<li>Scan the QR code or import the config file</li>
|
||||
<li>Toggle the connection on</li>
|
||||
</ol>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features */}
|
||||
{!showConfig && (
|
||||
<div className="mt-16 grid md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Zap className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Lightning Fast</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
WireGuard protocol for maximum speed
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Eye className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">No Logs Policy</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
We don't track or store your activity
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Lock className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Strong Encryption</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
ChaCha20 encryption for security
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center">
|
||||
<Globe className="w-8 h-8 text-primary mx-auto mb-3" />
|
||||
<h4 className="font-semibold mb-2">Multiple Locations</h4>
|
||||
<p className="text-sm text-muted-foreground">Servers across the globe</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user