This commit is contained in:
2026-01-19 08:32:44 +01:00
parent b4f6a83da0
commit 818779ab07
125 changed files with 32456 additions and 21017 deletions

View File

@@ -0,0 +1,471 @@
'use client'
import { useState } from 'react'
import { useQuery, useMutation } from '@tanstack/react-query'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'
import { DashboardLayout } from '@/components/layout/dashboard-layout'
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { settingsAPI } from '@/lib/api'
import { clearAuth } from '@/lib/auth'
import { usePlan } from '@/lib/use-plan'
export default function SettingsPage() {
const router = useRouter()
const [showPasswordForm, setShowPasswordForm] = useState(false)
const [showWebhookForm, setShowWebhookForm] = useState(false)
const [showSlackForm, setShowSlackForm] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const { canUseSlack, canUseWebhook } = usePlan()
const [passwordForm, setPasswordForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
})
const [webhookUrl, setWebhookUrl] = useState('')
const [slackWebhookUrl, setSlackWebhookUrl] = useState('')
const [deletePassword, setDeletePassword] = useState('')
// Fetch user settings
const { data: settings, isLoading, refetch } = useQuery({
queryKey: ['settings'],
queryFn: async () => {
const response = await settingsAPI.get()
setWebhookUrl(response.settings.webhookUrl || '')
setSlackWebhookUrl(response.settings.slackWebhookUrl || '')
return response.settings
},
})
// Change password mutation
const changePasswordMutation = useMutation({
mutationFn: async () => {
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
throw new Error('Passwords do not match')
}
if (passwordForm.newPassword.length < 8) {
throw new Error('Password must be at least 8 characters')
}
return settingsAPI.changePassword(passwordForm.currentPassword, passwordForm.newPassword)
},
onSuccess: () => {
toast.success('Password changed successfully')
setShowPasswordForm(false)
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
},
onError: (error: any) => {
toast.error(error.response?.data?.message || error.message || 'Failed to change password')
},
})
// Toggle email notifications
const toggleEmailMutation = useMutation({
mutationFn: async (enabled: boolean) => {
return settingsAPI.updateNotifications({ emailEnabled: enabled })
},
onSuccess: () => {
toast.success('Email notifications updated')
refetch()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update notifications')
},
})
// Update webhook
const updateWebhookMutation = useMutation({
mutationFn: async () => {
return settingsAPI.updateNotifications({
webhookUrl: webhookUrl || null,
webhookEnabled: !!webhookUrl,
})
},
onSuccess: () => {
toast.success('Webhook settings updated')
setShowWebhookForm(false)
refetch()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update webhook')
},
})
// Update Slack
const updateSlackMutation = useMutation({
mutationFn: async () => {
return settingsAPI.updateNotifications({
slackWebhookUrl: slackWebhookUrl || null,
slackEnabled: !!slackWebhookUrl,
})
},
onSuccess: () => {
toast.success('Slack integration updated')
setShowSlackForm(false)
refetch()
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to update Slack')
},
})
// Delete account mutation
const deleteAccountMutation = useMutation({
mutationFn: async () => {
return settingsAPI.deleteAccount(deletePassword)
},
onSuccess: () => {
toast.success('Account deleted successfully')
clearAuth()
router.push('/login')
},
onError: (error: any) => {
toast.error(error.response?.data?.message || 'Failed to delete account')
},
})
if (isLoading) {
return (
<DashboardLayout title="Settings" description="Manage your account and preferences">
<div className="flex items-center justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
</div>
</DashboardLayout>
)
}
return (
<DashboardLayout title="Settings" description="Manage your account and preferences">
{/* Account Settings */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Account</CardTitle>
<CardDescription>Manage your account settings</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Input
label="Email"
type="email"
value={settings?.email || ''}
disabled
/>
<div className="flex items-center gap-2">
<Badge>{settings?.plan || 'free'}</Badge>
<span className="text-sm text-muted-foreground">plan</span>
</div>
{!showPasswordForm ? (
<Button variant="outline" onClick={() => setShowPasswordForm(true)}>
Change Password
</Button>
) : (
<div className="space-y-3 rounded-lg border border-primary/20 bg-primary/5 p-4">
<Input
label="Current Password"
type="password"
value={passwordForm.currentPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, currentPassword: e.target.value })}
required
/>
<Input
label="New Password"
type="password"
value={passwordForm.newPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, newPassword: e.target.value })}
hint="At least 8 characters"
required
/>
<Input
label="Confirm New Password"
type="password"
value={passwordForm.confirmPassword}
onChange={(e) => setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })}
required
/>
<div className="flex gap-2">
<Button
onClick={() => changePasswordMutation.mutate()}
disabled={changePasswordMutation.isPending}
>
{changePasswordMutation.isPending ? 'Saving...' : 'Save Password'}
</Button>
<Button
variant="outline"
onClick={() => {
setShowPasswordForm(false)
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
}}
>
Cancel
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{/* Notifications */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>Configure how you receive alerts</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Email Notifications */}
<div className="flex items-center justify-between rounded-lg border border-border p-4">
<div>
<p className="font-medium">Email Notifications</p>
<p className="text-sm text-muted-foreground">Receive email alerts when changes are detected</p>
</div>
<Button
variant={settings?.emailEnabled !== false ? 'success' : 'outline'}
size="sm"
onClick={() => toggleEmailMutation.mutate(settings?.emailEnabled === false)}
disabled={toggleEmailMutation.isPending}
>
{settings?.emailEnabled !== false ? 'Enabled' : 'Disabled'}
</Button>
</div>
{/* Slack Integration */}
<div className="rounded-lg border border-border p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Slack Integration</p>
<p className="text-sm text-muted-foreground">Send alerts to your Slack workspace</p>
{settings?.slackEnabled && (
<p className="mt-1 text-xs text-green-600"> Configured</p>
)}
{!canUseSlack && (
<div className="mt-1 flex items-center gap-1.5 rounded bg-muted/50 px-2 py-0.5 w-fit">
<svg className="h-3 w-3 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">Pro Feature</span>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowSlackForm(!showSlackForm)}
disabled={!canUseSlack}
>
{settings?.slackEnabled ? 'Reconfigure' : 'Configure'}
</Button>
</div>
{showSlackForm && (
<div className="mt-4 space-y-3 rounded-lg border border-primary/20 bg-primary/5 p-3">
<Input
label="Slack Webhook URL"
type="url"
value={slackWebhookUrl}
onChange={(e) => setSlackWebhookUrl(e.target.value)}
placeholder="https://hooks.slack.com/services/..."
hint="Get this from your Slack app settings"
/>
<div className="flex gap-2">
<Button
onClick={() => updateSlackMutation.mutate()}
disabled={updateSlackMutation.isPending}
size="sm"
>
{updateSlackMutation.isPending ? 'Saving...' : 'Save'}
</Button>
<Button
variant="outline"
onClick={() => setShowSlackForm(false)}
size="sm"
>
Cancel
</Button>
{settings?.slackEnabled && (
<Button
variant="destructive"
onClick={() => {
setSlackWebhookUrl('')
updateSlackMutation.mutate()
}}
size="sm"
>
Remove
</Button>
)}
</div>
</div>
)}
</div>
{/* Webhook */}
<div className="rounded-lg border border-border p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Webhook</p>
<p className="text-sm text-muted-foreground">Send JSON payloads to your server</p>
{settings?.webhookEnabled && (
<p className="mt-1 text-xs text-green-600"> Configured</p>
)}
{!canUseWebhook && (
<div className="mt-1 flex items-center gap-1.5 rounded bg-muted/50 px-2 py-0.5 w-fit">
<svg className="h-3 w-3 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">Pro Feature</span>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowWebhookForm(!showWebhookForm)}
disabled={!canUseWebhook}
>
{settings?.webhookEnabled ? 'Reconfigure' : 'Configure'}
</Button>
</div>
{showWebhookForm && (
<div className="mt-4 space-y-3 rounded-lg border border-primary/20 bg-primary/5 p-3">
<Input
label="Webhook URL"
type="url"
value={webhookUrl}
onChange={(e) => setWebhookUrl(e.target.value)}
placeholder="https://your-server.com/webhook"
hint="We'll POST JSON data to this URL on changes"
/>
<div className="flex gap-2">
<Button
onClick={() => updateWebhookMutation.mutate()}
disabled={updateWebhookMutation.isPending}
size="sm"
>
{updateWebhookMutation.isPending ? 'Saving...' : 'Save'}
</Button>
<Button
variant="outline"
onClick={() => setShowWebhookForm(false)}
size="sm"
>
Cancel
</Button>
{settings?.webhookEnabled && (
<Button
variant="destructive"
onClick={() => {
setWebhookUrl('')
updateWebhookMutation.mutate()
}}
size="sm"
>
Remove
</Button>
)}
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
{/* Plan & Billing */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Plan & Billing</CardTitle>
<CardDescription>Manage your subscription</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between rounded-lg border border-primary/30 bg-primary/5 p-6">
<div>
<div className="flex items-center gap-2">
<p className="text-lg font-bold capitalize">{settings?.plan || 'Free'} Plan</p>
<Badge>Current</Badge>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{settings?.plan === 'free' && '5 monitors, 1hr frequency'}
{settings?.plan === 'pro' && '50 monitors, 5min frequency'}
{settings?.plan === 'business' && '200 monitors, 1min frequency'}
{settings?.plan === 'enterprise' && 'Unlimited monitors, all features'}
</p>
{settings?.plan !== 'free' && (
<p className="mt-2 text-sm text-muted-foreground">
Stripe Customer ID: {settings?.stripeCustomerId || 'N/A'}
</p>
)}
</div>
<Button variant="outline" disabled>
Manage Plan
</Button>
</div>
</CardContent>
</Card>
{/* Danger Zone */}
<Card className="border-destructive/30">
<CardHeader>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
<CardDescription>Irreversible actions</CardDescription>
</CardHeader>
<CardContent>
{!showDeleteConfirm ? (
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Delete Account</p>
<p className="text-sm text-muted-foreground">Permanently delete your account and all data</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
>
Delete Account
</Button>
</div>
) : (
<div className="space-y-3 rounded-lg border border-destructive/30 bg-destructive/5 p-4">
<div className="mb-2">
<p className="font-semibold text-destructive"> This action cannot be undone!</p>
<p className="text-sm text-muted-foreground">
All your monitors, snapshots, and alerts will be permanently deleted.
</p>
</div>
<Input
label="Confirm with your password"
type="password"
value={deletePassword}
onChange={(e) => setDeletePassword(e.target.value)}
placeholder="Enter your password"
/>
<div className="flex gap-2">
<Button
variant="destructive"
onClick={() => deleteAccountMutation.mutate()}
disabled={!deletePassword || deleteAccountMutation.isPending}
>
{deleteAccountMutation.isPending ? 'Deleting...' : 'Yes, Delete My Account'}
</Button>
<Button
variant="outline"
onClick={() => {
setShowDeleteConfirm(false)
setDeletePassword('')
}}
>
Cancel
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</DashboardLayout>
)
}