1171 lines
44 KiB
TypeScript
1171 lines
44 KiB
TypeScript
'use client'
|
|
import { useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { z } from 'zod'
|
|
import { useAuth } from '@/contexts/AuthContext'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
import { BalanceDisplay, TransactionHistory } from '@/components/balance'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import {
|
|
AlertCircle,
|
|
Save,
|
|
Plus,
|
|
Wallet,
|
|
CreditCard,
|
|
Download,
|
|
Eye,
|
|
Server,
|
|
FileText,
|
|
Activity,
|
|
X,
|
|
Settings,
|
|
Database,
|
|
Cloud,
|
|
Lock,
|
|
LogIn,
|
|
User,
|
|
} from 'lucide-react'
|
|
import { useProfileData } from '@/hooks/useProfileData'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
|
|
const ProfileUpdateSchema = z.object({
|
|
name: z.string().min(1, 'Name is required'),
|
|
email: z.string().email('Invalid email format'),
|
|
phone: z.string().optional(),
|
|
})
|
|
|
|
const PasswordChangeSchema = z
|
|
.object({
|
|
currentPassword: z.string().min(1, 'Current password is required'),
|
|
newPassword: z.string().min(6, 'Password must be at least 6 characters'),
|
|
confirmPassword: z.string().min(6, 'Password confirmation is required'),
|
|
})
|
|
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
message: "Passwords don't match",
|
|
path: ['confirmPassword'],
|
|
})
|
|
|
|
type ProfileUpdateData = z.infer<typeof ProfileUpdateSchema>
|
|
type PasswordChangeData = z.infer<typeof PasswordChangeSchema>
|
|
|
|
interface ProfileSettingsProps {
|
|
activeTab?: string
|
|
onTabChange?: (tab: string) => void
|
|
}
|
|
|
|
export function ProfileSettings({ activeTab = 'profile', onTabChange }: ProfileSettingsProps) {
|
|
const { user, logout } = useAuth()
|
|
const { profileStats, isLoading } = useProfileData()
|
|
const [isUpdating, setIsUpdating] = useState(false)
|
|
const [isChangingPassword, setIsChangingPassword] = useState(false)
|
|
const [updateMessage, setUpdateMessage] = useState<{
|
|
type: 'success' | 'error'
|
|
text: string
|
|
} | null>(null)
|
|
const [passwordMessage, setPasswordMessage] = useState<{
|
|
type: 'success' | 'error'
|
|
text: string
|
|
} | null>(null)
|
|
|
|
// Add Balance Modal State
|
|
const [isBalanceModalOpen, setIsBalanceModalOpen] = useState(false)
|
|
const [selectedCurrency, setSelectedCurrency] = useState<'INR' | 'USD'>('INR')
|
|
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
|
|
const [customAmount, setCustomAmount] = useState('')
|
|
const [balanceError, setBalanceError] = useState('')
|
|
const [isProcessingPayment, setIsProcessingPayment] = useState(false)
|
|
|
|
// Billing Details Modal State
|
|
const [isBillingModalOpen, setIsBillingModalOpen] = useState(false)
|
|
const [selectedBilling, setSelectedBilling] = useState<any>(null)
|
|
|
|
const profileForm = useForm<ProfileUpdateData>({
|
|
resolver: zodResolver(ProfileUpdateSchema),
|
|
defaultValues: {
|
|
name: user?.name || '',
|
|
email: user?.email || '',
|
|
phone: '',
|
|
},
|
|
})
|
|
|
|
const passwordForm = useForm<PasswordChangeData>({
|
|
resolver: zodResolver(PasswordChangeSchema),
|
|
})
|
|
|
|
const onProfileSubmit = async (data: ProfileUpdateData) => {
|
|
try {
|
|
setIsUpdating(true)
|
|
setUpdateMessage(null)
|
|
|
|
const response = await fetch('/api/user/profile', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (result.success) {
|
|
setUpdateMessage({ type: 'success', text: 'Profile updated successfully!' })
|
|
// Refresh user data
|
|
window.location.reload()
|
|
} else {
|
|
setUpdateMessage({
|
|
type: 'error',
|
|
text: result.error?.message || 'Failed to update profile',
|
|
})
|
|
}
|
|
} catch (error) {
|
|
setUpdateMessage({ type: 'error', text: 'An error occurred while updating your profile' })
|
|
} finally {
|
|
setIsUpdating(false)
|
|
}
|
|
}
|
|
|
|
const onPasswordSubmit = async (data: PasswordChangeData) => {
|
|
try {
|
|
setIsChangingPassword(true)
|
|
setPasswordMessage(null)
|
|
|
|
const response = await fetch('/api/user/change-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
currentPassword: data.currentPassword,
|
|
newPassword: data.newPassword,
|
|
}),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (result.success) {
|
|
setPasswordMessage({ type: 'success', text: 'Password changed successfully!' })
|
|
passwordForm.reset()
|
|
} else {
|
|
setPasswordMessage({
|
|
type: 'error',
|
|
text: result.error?.message || 'Failed to change password',
|
|
})
|
|
}
|
|
} catch (error) {
|
|
setPasswordMessage({ type: 'error', text: 'An error occurred while changing your password' })
|
|
} finally {
|
|
setIsChangingPassword(false)
|
|
}
|
|
}
|
|
|
|
const handleDeleteAccount = async () => {
|
|
if (
|
|
window.confirm('Are you sure you want to delete your account? This action cannot be undone.')
|
|
) {
|
|
try {
|
|
const response = await fetch('/api/user/delete', {
|
|
method: 'DELETE',
|
|
})
|
|
|
|
if (response.ok) {
|
|
await logout()
|
|
} else {
|
|
alert('Failed to delete account. Please try again.')
|
|
}
|
|
} catch (error) {
|
|
alert('An error occurred while deleting your account.')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Balance Modal Functions
|
|
const amountOptions = {
|
|
INR: [100, 500, 1000, 2000, 5000, 10000],
|
|
USD: [2, 10, 20, 50, 100, 200],
|
|
}
|
|
|
|
const handleOpenBalanceModal = () => {
|
|
setIsBalanceModalOpen(true)
|
|
setSelectedAmount(null)
|
|
setCustomAmount('')
|
|
setBalanceError('')
|
|
}
|
|
|
|
const handleCloseBalanceModal = () => {
|
|
setIsBalanceModalOpen(false)
|
|
setSelectedAmount(null)
|
|
setCustomAmount('')
|
|
setBalanceError('')
|
|
}
|
|
|
|
const handleCurrencyChange = (currency: 'INR' | 'USD') => {
|
|
setSelectedCurrency(currency)
|
|
setSelectedAmount(null)
|
|
setCustomAmount('')
|
|
setBalanceError('')
|
|
}
|
|
|
|
const handleAmountSelect = (amount: number) => {
|
|
setSelectedAmount(amount)
|
|
setCustomAmount(amount.toString())
|
|
setBalanceError('')
|
|
}
|
|
|
|
const handleCustomAmountChange = (value: string) => {
|
|
// Only allow numbers
|
|
const cleanValue = value.replace(/[^0-9]/g, '')
|
|
setCustomAmount(cleanValue)
|
|
setSelectedAmount(null)
|
|
setBalanceError('')
|
|
}
|
|
|
|
const validateAmount = () => {
|
|
if (!customAmount) {
|
|
setBalanceError('Please enter an amount')
|
|
return false
|
|
}
|
|
|
|
const amount = parseFloat(customAmount)
|
|
if (isNaN(amount) || amount <= 0) {
|
|
setBalanceError('Please enter a valid amount')
|
|
return false
|
|
}
|
|
|
|
// Validate amount limits (matching API validation)
|
|
const minAmount = selectedCurrency === 'INR' ? 100 : 2
|
|
const maxAmount = selectedCurrency === 'INR' ? 100000 : 1500
|
|
|
|
if (amount < minAmount) {
|
|
setBalanceError(`Minimum amount is ${selectedCurrency === 'INR' ? '₹' : '$'}${minAmount}`)
|
|
return false
|
|
}
|
|
|
|
if (amount > maxAmount) {
|
|
setBalanceError(`Maximum amount is ${selectedCurrency === 'INR' ? '₹' : '$'}${maxAmount}`)
|
|
return false
|
|
}
|
|
|
|
setBalanceError('')
|
|
return true
|
|
}
|
|
|
|
const handleSubmitBalance = async () => {
|
|
if (!validateAmount()) return
|
|
|
|
setIsProcessingPayment(true)
|
|
try {
|
|
const amount = parseFloat(customAmount)
|
|
|
|
// Call Add Balance API
|
|
const response = await fetch('/api/balance/add', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
amount,
|
|
currency: selectedCurrency,
|
|
}),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.message || 'Failed to initiate payment')
|
|
}
|
|
|
|
if (result.success) {
|
|
// Close modal before redirecting to payment gateway
|
|
handleCloseBalanceModal()
|
|
|
|
// Create and submit PayU form automatically
|
|
const form = document.createElement('form')
|
|
form.method = 'POST'
|
|
form.action = result.payment_url
|
|
|
|
// Add all PayU form fields
|
|
Object.entries(result.form_data).forEach(([key, value]) => {
|
|
const input = document.createElement('input')
|
|
input.type = 'hidden'
|
|
input.name = key
|
|
input.value = value as string
|
|
form.appendChild(input)
|
|
})
|
|
|
|
// Append form to body and submit
|
|
document.body.appendChild(form)
|
|
form.submit()
|
|
|
|
// Remove form after submission
|
|
document.body.removeChild(form)
|
|
} else {
|
|
throw new Error(result.message || 'Payment initiation failed')
|
|
}
|
|
} catch (error) {
|
|
console.error('Payment error:', error)
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : 'Failed to initiate payment. Please try again.'
|
|
setBalanceError(errorMessage)
|
|
} finally {
|
|
setIsProcessingPayment(false)
|
|
}
|
|
}
|
|
|
|
// Billing Details Modal Functions
|
|
const handleViewBilling = (billing: any) => {
|
|
setSelectedBilling(billing)
|
|
setIsBillingModalOpen(true)
|
|
}
|
|
|
|
const getServiceIcon = (serviceType: string) => {
|
|
const iconMap: Record<string, any> = {
|
|
hosting: Server,
|
|
kubernetes: Cloud,
|
|
database: Database,
|
|
domain: Server,
|
|
vps: Server,
|
|
cloud: Cloud,
|
|
}
|
|
const IconComponent = iconMap[serviceType.toLowerCase()] || Server
|
|
return <IconComponent className="w-5 h-5" />
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case 'paid':
|
|
return 'default'
|
|
case 'pending':
|
|
return 'secondary'
|
|
case 'failed':
|
|
return 'destructive'
|
|
default:
|
|
return 'secondary'
|
|
}
|
|
}
|
|
|
|
if (!user) return null
|
|
|
|
return (
|
|
<>
|
|
<Tabs value={activeTab} onValueChange={onTabChange} className="space-y-6">
|
|
<TabsList className="grid w-full grid-cols-6">
|
|
<TabsTrigger value="profile">Profile</TabsTrigger>
|
|
<TabsTrigger value="balance">Balance</TabsTrigger>
|
|
<TabsTrigger value="billing">Billing</TabsTrigger>
|
|
<TabsTrigger value="services">Services</TabsTrigger>
|
|
<TabsTrigger value="security">Security</TabsTrigger>
|
|
<TabsTrigger value="danger">Danger Zone</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="profile">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profile Information</CardTitle>
|
|
<CardDescription>Update your profile information and email address.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={profileForm.handleSubmit(onProfileSubmit)} className="space-y-4">
|
|
{updateMessage && (
|
|
<div
|
|
className={`p-3 text-sm rounded-md ${
|
|
updateMessage.type === 'success'
|
|
? 'text-green-600 bg-green-50 border border-green-200'
|
|
: 'text-red-600 bg-red-50 border border-red-200'
|
|
}`}
|
|
>
|
|
{updateMessage.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Full Name</Label>
|
|
<Input id="name" {...profileForm.register('name')} disabled={isUpdating} />
|
|
{profileForm.formState.errors.name && (
|
|
<p className="text-sm text-red-600">
|
|
{profileForm.formState.errors.name.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email Address</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
{...profileForm.register('email')}
|
|
disabled={isUpdating}
|
|
/>
|
|
{profileForm.formState.errors.email && (
|
|
<p className="text-sm text-red-600">
|
|
{profileForm.formState.errors.email.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="phone">Phone Number (Optional)</Label>
|
|
<Input
|
|
id="phone"
|
|
type="tel"
|
|
placeholder="+1 (555) 123-4567"
|
|
{...profileForm.register('phone')}
|
|
disabled={isUpdating}
|
|
/>
|
|
{profileForm.formState.errors.phone && (
|
|
<p className="text-sm text-red-600">
|
|
{profileForm.formState.errors.phone.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="siliconId">Silicon ID</Label>
|
|
<Input
|
|
id="siliconId"
|
|
value={user.siliconId || ''}
|
|
disabled
|
|
className="bg-muted"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Your unique identifier in our system
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Button type="submit" disabled={isUpdating}>
|
|
<Save className="w-4 h-4 mr-2" />
|
|
{isUpdating ? 'Updating...' : 'Update Profile'}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="balance" className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Wallet className="w-5 h-5" />
|
|
Account Balance
|
|
</CardTitle>
|
|
<CardDescription>Manage your account funds and transactions.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg p-6 text-white mb-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-blue-100">Current Balance</p>
|
|
{isLoading ? (
|
|
<Skeleton className="h-9 w-32 bg-white/20" />
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<Wallet className="w-5 h-5" />
|
|
<BalanceDisplay
|
|
className="text-3xl font-bold"
|
|
variant="default"
|
|
showIcon={false}
|
|
/>
|
|
</div>
|
|
|
|
// <p className="text-3xl font-bold">
|
|
// ₹
|
|
// {profileStats?.balance?.toLocaleString('en-IN', {
|
|
// minimumFractionDigits: 2,
|
|
// }) || '1,250.00'}
|
|
// </p>
|
|
)}
|
|
</div>
|
|
<div className="text-right">
|
|
<Button
|
|
className="bg-white/20 hover:bg-white/30 text-white border-white/30"
|
|
onClick={handleOpenBalanceModal}
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Funds
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Activity className="w-5 h-5" />
|
|
Transaction History
|
|
</CardTitle>
|
|
<CardDescription>View all your transactions and balance changes</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<TransactionHistory />
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="billing">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<FileText className="w-5 h-5" />
|
|
Billing History
|
|
</CardTitle>
|
|
<CardDescription>
|
|
View and manage your billing information and invoices.
|
|
</CardDescription>
|
|
</div>
|
|
<Button>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
New Service
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{[
|
|
{
|
|
id: 'INV-001',
|
|
service: 'VPS Hosting Plan',
|
|
amount: '599.00',
|
|
date: 'Dec 15, 2024',
|
|
dueDate: 'Dec 15, 2024',
|
|
status: 'paid',
|
|
serviceType: 'hosting',
|
|
billingCycle: 'Monthly',
|
|
description: 'VPS hosting service with 4GB RAM, 2 vCPUs, and 80GB SSD storage',
|
|
},
|
|
{
|
|
id: 'INV-002',
|
|
service: 'Kubernetes Cluster',
|
|
amount: '299.00',
|
|
date: 'Dec 5, 2024',
|
|
dueDate: 'Dec 5, 2024',
|
|
status: 'paid',
|
|
serviceType: 'kubernetes',
|
|
billingCycle: 'Monthly',
|
|
description: 'Managed Kubernetes cluster with 3 nodes and load balancer',
|
|
},
|
|
{
|
|
id: 'INV-003',
|
|
service: 'Domain Registration',
|
|
amount: '199.00',
|
|
date: 'Nov 28, 2024',
|
|
dueDate: 'Dec 5, 2024',
|
|
status: 'pending',
|
|
serviceType: 'domain',
|
|
billingCycle: 'Annual',
|
|
description: 'Domain registration for .com domain with DNS management',
|
|
},
|
|
].map((invoice) => (
|
|
<div
|
|
key={invoice.id}
|
|
className="flex items-center justify-between p-4 border rounded-lg"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-muted rounded-lg flex items-center justify-center">
|
|
{getServiceIcon(invoice.serviceType)}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{invoice.service}</p>
|
|
<p className="text-sm text-muted-foreground">ID: {invoice.id}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-right">
|
|
<p className="font-semibold">₹{invoice.amount}</p>
|
|
<p className="text-sm text-muted-foreground">{invoice.date}</p>
|
|
</div>
|
|
<Badge variant={getStatusColor(invoice.status)}>
|
|
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
|
</Badge>
|
|
<div className="flex gap-1">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => handleViewBilling(invoice)}
|
|
title="View Details"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
{invoice.status === 'pending' && (
|
|
<Button size="sm" variant="ghost" title="Pay Now">
|
|
<CreditCard className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
<Button size="sm" variant="ghost" title="Download PDF">
|
|
<Download className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="services">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Server className="w-5 h-5" />
|
|
Active Services
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Manage your active hosting services and configurations.
|
|
</CardDescription>
|
|
</div>
|
|
<Button>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Add Service
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{(() => {
|
|
const activeServices = [
|
|
{
|
|
name: 'VPS Hosting Plan',
|
|
id: 'VPS-001',
|
|
serviceId: 'vps_12345',
|
|
billingId: 'bill_67890',
|
|
clusterId: null,
|
|
startDate: 'Dec 15, 2024',
|
|
nextBilling: 'Jan 15, 2025',
|
|
status: 'active',
|
|
type: 'hosting',
|
|
},
|
|
{
|
|
name: 'Kubernetes Cluster',
|
|
id: 'K8S-001',
|
|
serviceId: 'k8s_54321',
|
|
billingId: 'bill_98765',
|
|
clusterId: 'cluster_abc123',
|
|
startDate: 'Dec 5, 2024',
|
|
nextBilling: 'Jan 5, 2025',
|
|
status: 'active',
|
|
type: 'kubernetes',
|
|
},
|
|
{
|
|
name: 'Database Service',
|
|
id: 'DB-001',
|
|
serviceId: 'db_11111',
|
|
billingId: 'bill_22222',
|
|
clusterId: null,
|
|
startDate: 'Nov 20, 2024',
|
|
nextBilling: 'Dec 20, 2024',
|
|
status: 'active',
|
|
type: 'database',
|
|
},
|
|
]
|
|
|
|
return activeServices.length > 0 ? (
|
|
activeServices.map((service) => (
|
|
<div
|
|
key={service.id}
|
|
className="flex items-center justify-between p-4 border rounded-lg"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-muted rounded-lg flex items-center justify-center">
|
|
{getServiceIcon(service.type)}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{service.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
Service ID: {service.serviceId}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-right text-sm">
|
|
<p>Started: {service.startDate}</p>
|
|
<p className="text-muted-foreground">
|
|
Next billing: {service.nextBilling}
|
|
</p>
|
|
</div>
|
|
<Badge variant="default">Active</Badge>
|
|
<div className="flex gap-1">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
title="Service Settings"
|
|
onClick={() => alert(`Configure ${service.name}`)}
|
|
>
|
|
<Settings className="w-4 h-4" />
|
|
</Button>
|
|
{service.type === 'kubernetes' && service.clusterId && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
title="Download Kubernetes Config"
|
|
onClick={() => {
|
|
window.open(
|
|
`/api/services/download-kubernetes?cluster_id=${service.clusterId}`,
|
|
'_blank'
|
|
)
|
|
}}
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
{service.type === 'hosting' && service.billingId && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
title="Download Hosting Config"
|
|
onClick={() => {
|
|
window.open(
|
|
`/api/services/download-hosting-conf?billing_id=${service.billingId}`,
|
|
'_blank'
|
|
)
|
|
}}
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<Server className="w-16 h-16 text-muted-foreground/50 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">No Active Services</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
You don't have any active services at the moment
|
|
</p>
|
|
<Button>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Browse Services
|
|
</Button>
|
|
</div>
|
|
)
|
|
})()}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="security">
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Change Password</CardTitle>
|
|
<CardDescription>
|
|
{user.provider === 'local'
|
|
? 'Update your password to keep your account secure.'
|
|
: `You're signed in with ${user.provider}. Password changes are not available for OAuth accounts.`}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{user.provider === 'local' ? (
|
|
<form
|
|
onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}
|
|
className="space-y-4"
|
|
>
|
|
{passwordMessage && (
|
|
<div
|
|
className={`p-3 text-sm rounded-md ${
|
|
passwordMessage.type === 'success'
|
|
? 'text-green-600 bg-green-50 border border-green-200'
|
|
: 'text-red-600 bg-red-50 border border-red-200'
|
|
}`}
|
|
>
|
|
{passwordMessage.text}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="currentPassword">Current Password</Label>
|
|
<Input
|
|
id="currentPassword"
|
|
type="password"
|
|
{...passwordForm.register('currentPassword')}
|
|
disabled={isChangingPassword}
|
|
/>
|
|
{passwordForm.formState.errors.currentPassword && (
|
|
<p className="text-sm text-red-600">
|
|
{passwordForm.formState.errors.currentPassword.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newPassword">New Password</Label>
|
|
<Input
|
|
id="newPassword"
|
|
type="password"
|
|
{...passwordForm.register('newPassword')}
|
|
disabled={isChangingPassword}
|
|
/>
|
|
{passwordForm.formState.errors.newPassword && (
|
|
<p className="text-sm text-red-600">
|
|
{passwordForm.formState.errors.newPassword.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
|
<Input
|
|
id="confirmPassword"
|
|
type="password"
|
|
{...passwordForm.register('confirmPassword')}
|
|
disabled={isChangingPassword}
|
|
/>
|
|
{passwordForm.formState.errors.confirmPassword && (
|
|
<p className="text-sm text-red-600">
|
|
{passwordForm.formState.errors.confirmPassword.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Button type="submit" disabled={isChangingPassword}>
|
|
{isChangingPassword ? 'Changing Password...' : 'Change Password'}
|
|
</Button>
|
|
</form>
|
|
) : (
|
|
<div className="text-muted-foreground">
|
|
Password management is handled by your OAuth provider ({user.provider}).
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Activity className="w-5 h-5" />
|
|
Security Activity
|
|
</CardTitle>
|
|
<CardDescription>Recent account activity and login history.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{(() => {
|
|
const securityActivities = [
|
|
{
|
|
action: 'Successful login',
|
|
date: 'Today at 10:30 AM',
|
|
ip: '192.168.1.1',
|
|
location: 'New York, US',
|
|
icon: 'login',
|
|
type: 'success',
|
|
},
|
|
{
|
|
action: 'Profile updated',
|
|
date: '2 days ago',
|
|
ip: '192.168.1.1',
|
|
location: 'New York, US',
|
|
icon: 'user',
|
|
type: 'info',
|
|
},
|
|
{
|
|
action: 'Password changed',
|
|
date: '3 days ago',
|
|
ip: '192.168.1.1',
|
|
location: 'New York, US',
|
|
icon: 'lock',
|
|
type: 'success',
|
|
},
|
|
{
|
|
action: 'Failed login attempt',
|
|
date: '1 week ago',
|
|
ip: '203.0.113.1',
|
|
location: 'Unknown',
|
|
icon: 'warning',
|
|
type: 'warning',
|
|
},
|
|
{
|
|
action: 'Account accessed',
|
|
date: '2 weeks ago',
|
|
ip: '192.168.1.2',
|
|
location: 'Boston, US',
|
|
icon: 'login',
|
|
type: 'info',
|
|
},
|
|
{
|
|
action: 'Email verification',
|
|
date: '3 weeks ago',
|
|
ip: '192.168.1.1',
|
|
location: 'New York, US',
|
|
icon: 'user',
|
|
type: 'success',
|
|
},
|
|
]
|
|
|
|
return securityActivities.map((activity, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between p-4 border rounded-lg"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
|
activity.type === 'warning'
|
|
? 'bg-red-100'
|
|
: activity.type === 'success'
|
|
? 'bg-green-100'
|
|
: 'bg-muted'
|
|
}`}
|
|
>
|
|
{activity.icon === 'lock' && (
|
|
<Lock
|
|
className={`w-4 h-4 ${activity.type === 'success' ? 'text-green-600' : ''}`}
|
|
/>
|
|
)}
|
|
{activity.icon === 'login' && (
|
|
<LogIn
|
|
className={`w-4 h-4 ${activity.type === 'success' ? 'text-green-600' : ''}`}
|
|
/>
|
|
)}
|
|
{activity.icon === 'user' && (
|
|
<User
|
|
className={`w-4 h-4 ${activity.type === 'success' ? 'text-green-600' : ''}`}
|
|
/>
|
|
)}
|
|
{activity.icon === 'warning' && (
|
|
<AlertCircle className="w-4 h-4 text-red-600" />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{activity.action}</p>
|
|
<p className="text-sm text-muted-foreground">{activity.date}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right text-sm text-muted-foreground">
|
|
<p>{activity.location}</p>
|
|
<p className="text-xs">{activity.ip}</p>
|
|
</div>
|
|
</div>
|
|
))
|
|
})()}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="danger">
|
|
<Card className="border-red-200">
|
|
<CardHeader>
|
|
<CardTitle className="text-red-600">Danger Zone</CardTitle>
|
|
<CardDescription>
|
|
These actions are irreversible. Please proceed with caution.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="flex items-start justify-between p-4 border rounded-md">
|
|
<div className="flex-1">
|
|
<h4 className="text-sm font-medium">Export Account Data</h4>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Download all your data in a portable format. This includes your profile
|
|
information, billing history, and service configurations.
|
|
</p>
|
|
</div>
|
|
<Button variant="outline" className="ml-4">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Export Data
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex items-start space-x-3 p-4 border border-red-200 rounded-md bg-red-50">
|
|
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5" />
|
|
<div className="flex-1">
|
|
<h4 className="text-sm font-medium text-red-800">Delete Account</h4>
|
|
<p className="text-sm text-red-700 mt-1">
|
|
Once you delete your account, there is no going back. This will permanently
|
|
delete your account and all associated data including active services.
|
|
</p>
|
|
<Button variant="destructive" className="mt-3" onClick={handleDeleteAccount}>
|
|
Delete Account
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Add Balance Modal */}
|
|
<Dialog open={isBalanceModalOpen} onOpenChange={setIsBalanceModalOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl">Add Balance</DialogTitle>
|
|
<DialogDescription>
|
|
Choose an amount or enter a custom amount to add to your balance
|
|
</DialogDescription>
|
|
{balanceError && <div className="text-sm text-red-600 mt-2">{balanceError}</div>}
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* Currency Selection */}
|
|
<div className="flex gap-3">
|
|
<Button
|
|
type="button"
|
|
variant={selectedCurrency === 'INR' ? 'default' : 'outline'}
|
|
onClick={() => handleCurrencyChange('INR')}
|
|
>
|
|
INR (₹)
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant={selectedCurrency === 'USD' ? 'default' : 'outline'}
|
|
onClick={() => handleCurrencyChange('USD')}
|
|
>
|
|
USD ($)
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Amount Selection */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{amountOptions[selectedCurrency].map((amount) => (
|
|
<Button
|
|
key={amount}
|
|
type="button"
|
|
variant={selectedAmount === amount ? 'default' : 'outline'}
|
|
onClick={() => handleAmountSelect(amount)}
|
|
className="text-sm"
|
|
>
|
|
{selectedCurrency === 'INR' ? '₹' : '$'}
|
|
{amount}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Custom Amount Input */}
|
|
<div className="flex items-center gap-2 border rounded-md p-3 bg-muted/20">
|
|
<span className="text-sm text-muted-foreground">You've selected</span>
|
|
<span className="text-lg font-bold">{selectedCurrency === 'INR' ? '₹' : '$'}</span>
|
|
<Input
|
|
type="text"
|
|
placeholder="Amount"
|
|
value={customAmount}
|
|
onChange={(e) => handleCustomAmountChange(e.target.value)}
|
|
className="text-lg font-bold text-center border-0 bg-transparent p-0 focus:ring-0"
|
|
/>
|
|
<span className="text-sm text-muted-foreground">to top up your balance.</span>
|
|
</div>
|
|
|
|
{/* Modal Actions */}
|
|
<div className="flex gap-3 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={handleCloseBalanceModal}
|
|
className="flex-1"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={handleSubmitBalance}
|
|
disabled={!customAmount || isProcessingPayment}
|
|
className="flex-1"
|
|
>
|
|
{isProcessingPayment ? 'Processing...' : 'Next'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Billing Details Modal */}
|
|
<Dialog open={isBillingModalOpen} onOpenChange={setIsBillingModalOpen}>
|
|
<DialogContent className="sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl">Billing Details</DialogTitle>
|
|
<DialogDescription>Invoice and billing information</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{selectedBilling && (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="text-lg font-semibold">Invoice #{selectedBilling.id}</h4>
|
|
<Badge variant={getStatusColor(selectedBilling.status)}>
|
|
{selectedBilling.status.charAt(0).toUpperCase() + selectedBilling.status.slice(1)}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Date Created</p>
|
|
<p className="font-medium">{selectedBilling.date}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Due Date</p>
|
|
<p className="font-medium">{selectedBilling.dueDate}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Service</p>
|
|
<p className="font-medium">{selectedBilling.service}</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Amount</p>
|
|
<p className="text-xl font-bold">₹{selectedBilling.amount}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Billing Cycle</p>
|
|
<p className="font-medium">{selectedBilling.billingCycle}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedBilling.description && (
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Description</p>
|
|
<p className="text-sm">{selectedBilling.description}</p>
|
|
</div>
|
|
)}
|
|
|
|
{selectedBilling.status === 'pending' && (
|
|
<div className="flex gap-3 pt-4">
|
|
<Button className="flex-1">
|
|
<CreditCard className="w-4 h-4 mr-2" />
|
|
Pay Now
|
|
</Button>
|
|
<Button variant="outline">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Download
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{selectedBilling.status === 'paid' && (
|
|
<div className="flex justify-center pt-4">
|
|
<Button variant="outline">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Download PDF
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|