Add settings page with profile, billing, notifications, security tabs

- Create comprehensive settings page with 4 tabs
- Profile: name editing with save functionality
- Billing: subscription display with Stripe portal link
- Notifications: toggle switches for email preferences
- Security: password change, 2FA placeholder, account deletion
- Add shadcn Switch component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 12:37:42 +01:00
parent 54e05173e0
commit eff740704b
4 changed files with 599 additions and 0 deletions

View File

@@ -0,0 +1,384 @@
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
import { Switch } from '@/components/ui/switch'
import { Separator } from '@/components/ui/separator'
import { User, CreditCard, Bell, Shield, Loader2, Check, ExternalLink } from 'lucide-react'
import type { Database } from '@/types/database'
type Profile = Database['public']['Tables']['profiles']['Row']
type Subscription = Database['public']['Tables']['subscriptions']['Row']
export default function SettingsPage() {
const [profile, setProfile] = useState<Profile | null>(null)
const [subscription, setSubscription] = useState<Subscription | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [formData, setFormData] = useState({
full_name: '',
email: '',
})
const [preferences, setPreferences] = useState({
emailNotifications: true,
marketingEmails: false,
weeklyDigest: true,
})
const supabase = createClient()
useEffect(() => {
async function loadData() {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [profileRes, subRes] = await Promise.all([
(supabase as any).from('profiles').select('*').eq('id', user.id).single(),
(supabase as any).from('subscriptions').select('*').eq('user_id', user.id).single(),
])
if (profileRes.data) {
setProfile(profileRes.data)
setFormData({
full_name: profileRes.data.full_name || '',
email: profileRes.data.email || '',
})
}
if (subRes.data) {
setSubscription(subRes.data)
}
setLoading(false)
}
loadData()
}, [supabase])
const handleSaveProfile = async () => {
if (!profile) return
setSaving(true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error } = await (supabase as any)
.from('profiles')
.update({ full_name: formData.full_name })
.eq('id', profile.id)
setSaving(false)
if (!error) {
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
}
const handleManageBilling = async () => {
const response = await fetch('/api/stripe/portal', {
method: 'POST',
})
const data = await response.json()
if (data.url) {
window.location.href = data.url
}
}
if (loading) {
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center justify-center py-16">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</div>
)
}
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
<p className="text-muted-foreground">Manage your account and preferences</p>
</div>
<Tabs defaultValue="profile" className="space-y-6">
<TabsList className="grid w-full grid-cols-4 lg:w-auto lg:inline-grid">
<TabsTrigger value="profile" className="gap-2">
<User className="h-4 w-4" />
<span className="hidden sm:inline">Profile</span>
</TabsTrigger>
<TabsTrigger value="billing" className="gap-2">
<CreditCard className="h-4 w-4" />
<span className="hidden sm:inline">Billing</span>
</TabsTrigger>
<TabsTrigger value="notifications" className="gap-2">
<Bell className="h-4 w-4" />
<span className="hidden sm:inline">Notifications</span>
</TabsTrigger>
<TabsTrigger value="security" className="gap-2">
<Shield className="h-4 w-4" />
<span className="hidden sm:inline">Security</span>
</TabsTrigger>
</TabsList>
<TabsContent value="profile">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
value={formData.full_name}
onChange={(e) => setFormData({ ...formData, full_name: e.target.value })}
placeholder="Enter your name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
value={formData.email}
disabled
className="bg-muted"
/>
<p className="text-xs text-muted-foreground">
Contact support to change your email
</p>
</div>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Account Role</p>
<p className="text-sm text-muted-foreground">
Your current account permissions
</p>
</div>
<Badge variant={profile?.role === 'admin' ? 'brand' : 'secondary'}>
{profile?.role || 'user'}
</Badge>
</div>
<div className="flex justify-end">
<Button onClick={handleSaveProfile} disabled={saving}>
{saving ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : saved ? (
<Check className="h-4 w-4 mr-2" />
) : null}
{saved ? 'Saved!' : 'Save Changes'}
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="billing">
<Card>
<CardHeader>
<CardTitle>Subscription & Billing</CardTitle>
<CardDescription>Manage your subscription and payment methods</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<div className="flex items-center gap-2">
<p className="font-medium">Current Plan</p>
<Badge variant="brand">{subscription?.plan || 'Free'}</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">
{subscription?.status === 'active' ? (
<>
Renews on{' '}
{subscription.current_period_end
? new Date(subscription.current_period_end).toLocaleDateString()
: 'N/A'}
</>
) : (
'Upgrade to unlock more features'
)}
</p>
</div>
{subscription?.plan === 'free' ? (
<Button variant="brand" asChild>
<a href="/pricing">Upgrade Plan</a>
</Button>
) : (
<Button variant="outline" onClick={handleManageBilling}>
<ExternalLink className="h-4 w-4 mr-2" />
Manage Billing
</Button>
)}
</div>
<Separator />
<div>
<h4 className="font-medium mb-4">Plan Features</h4>
<div className="grid gap-3 sm:grid-cols-2">
{subscription?.plan === 'free' ? (
<>
<PlanFeature label="Projects" value="1" />
<PlanFeature label="AI Messages" value="50/month" />
<PlanFeature label="Templates" value="Basic" />
<PlanFeature label="Support" value="Community" />
</>
) : subscription?.plan === 'pro' ? (
<>
<PlanFeature label="Projects" value="Unlimited" />
<PlanFeature label="AI Messages" value="2,000/month" />
<PlanFeature label="Templates" value="All" />
<PlanFeature label="Support" value="Priority" />
</>
) : (
<>
<PlanFeature label="Projects" value="Unlimited" />
<PlanFeature label="AI Messages" value="10,000/month" />
<PlanFeature label="Team Members" value="Unlimited" />
<PlanFeature label="Support" value="Dedicated" />
</>
)}
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="notifications">
<Card>
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
<CardDescription>Choose how you want to be notified</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<NotificationToggle
label="Email Notifications"
description="Receive email updates about your projects"
checked={preferences.emailNotifications}
onCheckedChange={(checked) =>
setPreferences({ ...preferences, emailNotifications: checked })
}
/>
<Separator />
<NotificationToggle
label="Weekly Digest"
description="Get a weekly summary of your project activity"
checked={preferences.weeklyDigest}
onCheckedChange={(checked) =>
setPreferences({ ...preferences, weeklyDigest: checked })
}
/>
<Separator />
<NotificationToggle
label="Marketing Emails"
description="Receive updates about new features and offers"
checked={preferences.marketingEmails}
onCheckedChange={(checked) =>
setPreferences({ ...preferences, marketingEmails: checked })
}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="security">
<Card>
<CardHeader>
<CardTitle>Security Settings</CardTitle>
<CardDescription>Manage your account security</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Password</p>
<p className="text-sm text-muted-foreground">
Last changed: Never
</p>
</div>
<Button variant="outline">Change Password</Button>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Two-Factor Authentication</p>
<p className="text-sm text-muted-foreground">
Add an extra layer of security
</p>
</div>
<Badge variant="outline">Coming Soon</Badge>
</div>
<Separator />
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Active Sessions</p>
<p className="text-sm text-muted-foreground">
1 active session
</p>
</div>
<Button variant="outline">View Sessions</Button>
</div>
<Separator />
<div className="pt-4">
<p className="font-medium text-destructive">Danger Zone</p>
<p className="text-sm text-muted-foreground mb-4">
Permanently delete your account and all data
</p>
<Button variant="destructive">Delete Account</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}
function PlanFeature({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<span className="text-sm text-muted-foreground">{label}</span>
<span className="font-medium">{value}</span>
</div>
)
}
function NotificationToggle({
label,
description,
checked,
onCheckedChange,
}: {
label: string
description: string
checked: boolean
onCheckedChange: (checked: boolean) => void
}) {
return (
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{label}</p>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<Switch checked={checked} onCheckedChange={onCheckedChange} />
</div>
)
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }