feat: merge account into settings layout with billing/profile/security/support

- account/layout.tsx: section header + route tabs (Billing & plan first,
  since the subscription is the most-visited account content), same RouteTabs
  interaction grammar as messaging and webhooks; active section survives
  refresh with shareable URLs
- /dashboard/account redirects to /billing (SubscriptionInfo + plan CTAs);
  /profile, /security and /support are thin pages reusing the existing forms
- Security combines password change with the delete-account flow in a
  clearly separated destructive "Danger zone" card
- legacy routes (edit-profile, change-password, delete-account, get-support)
  redirect into the merged sections so old links keep working
- account e2e: billing redirect + mocked subscription, security deep-link
  refresh survival, legacy redirects

Build, 28 unit tests, and 16 e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
isra el
2026-07-11 07:54:26 +03:00
parent 8a5f12a6eb
commit f5815e1069
11 changed files with 223 additions and 206 deletions

View File

@@ -0,0 +1,10 @@
import SubscriptionInfo from '../../(components)/subscription-info'
// Billing & plan: current subscription, usage limits and plan CTAs.
export default function BillingPage() {
return (
<div className='max-w-2xl'>
<SubscriptionInfo />
</div>
)
}

View File

@@ -1,29 +1,6 @@
import { ShieldIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import ChangePasswordForm from '../../(components)/change-password-form'
import { redirect } from 'next/navigation'
// Legacy route: password management now lives in Account > Security.
export default function ChangePasswordPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<ShieldIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Change Password</h2>
</div>
<p className='text-muted-foreground'>Update your account password</p>
</div>
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Password Security</CardTitle>
<CardDescription>Change your password to keep your account secure</CardDescription>
</CardHeader>
<CardContent>
<ChangePasswordForm />
</CardContent>
</Card>
</div>
</div>
)
}
redirect('/dashboard/account/security')
}

View File

@@ -1,34 +1,6 @@
import { AlertTriangleIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import DeleteAccountForm from '../../(components)/delete-account-form'
import { redirect } from 'next/navigation'
export default function DangerZonePage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<AlertTriangleIcon className='h-6 w-6 text-destructive' />
<h2 className='text-3xl font-bold tracking-tight'>Danger Zone</h2>
</div>
<p className='text-muted-foreground'>Manage critical account actions</p>
</div>
<div className='max-w-2xl'>
<Card className='border-destructive/50'>
<CardHeader>
<div className='flex items-center gap-2 text-destructive'>
<AlertTriangleIcon className='h-5 w-5' />
<CardTitle>Delete Account</CardTitle>
</div>
<CardDescription>
Permanently delete your account and all associated data
</CardDescription>
</CardHeader>
<CardContent>
<DeleteAccountForm />
</CardContent>
</Card>
</div>
</div>
)
}
// Legacy route: account deletion now lives in Account > Security (danger zone).
export default function DeleteAccountPage() {
redirect('/dashboard/account/security')
}

View File

@@ -1,29 +1,6 @@
import { UserIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import EditProfileForm from '../../(components)/edit-profile-form'
import { redirect } from 'next/navigation'
// Legacy route: profile editing now lives in the merged account settings.
export default function EditProfilePage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<UserIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Edit Profile</h2>
</div>
<p className='text-muted-foreground'>Update your profile information</p>
</div>
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your personal details</CardDescription>
</CardHeader>
<CardContent>
<EditProfileForm />
</CardContent>
</Card>
</div>
</div>
)
}
redirect('/dashboard/account/profile')
}

View File

@@ -1,29 +1,6 @@
import { MessageSquareIcon } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import SupportForm from '../../(components)/support-form'
import { redirect } from 'next/navigation'
// Legacy route: support now lives in Account > Support.
export default function GetSupportPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<MessageSquareIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Get Support</h2>
</div>
<p className='text-muted-foreground'>Contact our support team for assistance</p>
</div>
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Contact Support</CardTitle>
<CardDescription>Fill out the form below and we'll get back to you as soon as possible.</CardDescription>
</CardHeader>
<CardContent>
<SupportForm />
</CardContent>
</Card>
</div>
</div>
)
}
redirect('/dashboard/account/support')
}

View File

@@ -0,0 +1,36 @@
import type { PropsWithChildren } from 'react'
import { UserIcon } from 'lucide-react'
import RouteTabs from '@/components/shared/route-tabs'
// Account is one settings experience: sections are route-based tabs (same
// interaction grammar as messaging/webhooks), Billing first since the
// subscription is the most-visited account content.
export default function AccountLayout({ children }: PropsWithChildren) {
return (
<div className='flex-1 p-4 sm:p-6 md:p-8'>
<div className='mb-4 space-y-1'>
<div className='flex items-center space-x-2'>
<UserIcon className='h-6 w-6 text-primary' />
<h2 className='text-2xl sm:text-3xl font-bold tracking-tight'>
Account
</h2>
</div>
<p className='text-muted-foreground'>
Manage your subscription, profile and security
</p>
</div>
<RouteTabs
className='mb-6'
tabs={[
{ href: '/dashboard/account/billing', label: 'Billing & plan' },
{ href: '/dashboard/account/profile', label: 'Profile' },
{ href: '/dashboard/account/security', label: 'Security' },
{ href: '/dashboard/account/support', label: 'Support' },
]}
/>
{children}
</div>
)
}

View File

@@ -1,95 +1,6 @@
import { UserIcon, PencilIcon, KeyIcon, AlertTriangleIcon, MessageSquareIcon } from 'lucide-react'
import SubscriptionInfo from '../(components)/subscription-info'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import { redirect } from 'next/navigation'
// Account is a settings section with subroutes; Billing is the default view.
export default function AccountPage() {
return (
<div className='flex-1 space-y-6 p-6 md:p-8'>
<div className='space-y-1'>
<div className='flex items-center space-x-2'>
<UserIcon className='h-6 w-6 text-primary' />
<h2 className='text-3xl font-bold tracking-tight'>Account</h2>
</div>
<p className='text-muted-foreground'>Manage your account settings and preferences</p>
</div>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-6'>
{/* Left column - Subscription Information */}
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Subscription Information</h3>
<SubscriptionInfo />
</div>
{/* Right column - Account Management */}
<div className='space-y-6'>
<div className='space-y-4'>
<h3 className='text-lg font-semibold'>Account Management</h3>
<div className='grid gap-4'>
<Link href="/dashboard/account/edit-profile">
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className='flex items-center'>
<div className='bg-primary/10 p-2 rounded-full mr-3'>
<PencilIcon className='h-5 w-5 text-primary' />
</div>
<div className='text-left'>
<div className='font-medium'>Edit Profile</div>
<div className='text-sm text-muted-foreground'>Update your personal information</div>
</div>
</div>
</Button>
</Link>
<Link href="/dashboard/account/change-password">
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className='flex items-center'>
<div className='bg-primary/10 p-2 rounded-full mr-3'>
<KeyIcon className='h-5 w-5 text-primary' />
</div>
<div className='text-left'>
<div className='font-medium'>Change Password</div>
<div className='text-sm text-muted-foreground'>Update your account password</div>
</div>
</div>
</Button>
</Link>
<Link href="/dashboard/account/get-support">
<Button variant="outline" className="w-full justify-start h-auto py-3">
<div className='flex items-center'>
<div className='bg-primary/10 p-2 rounded-full mr-3'>
<MessageSquareIcon className='h-5 w-5 text-primary' />
</div>
<div className='text-left'>
<div className='font-medium'>Get Support</div>
<div className='text-sm text-muted-foreground'>Contact our support team for assistance</div>
</div>
</div>
</Button>
</Link>
</div>
</div>
<div className='space-y-4 pt-4 border-t'>
<h3 className='text-lg font-semibold text-destructive'>Danger Zone</h3>
<Link href="/dashboard/account/delete-account">
<Button variant="outline" className="w-full justify-start h-auto py-3 border-destructive/30">
<div className='flex items-center'>
<div className='bg-destructive/10 p-2 rounded-full mr-3'>
<AlertTriangleIcon className='h-5 w-5 text-destructive' />
</div>
<div className='text-left'>
<div className='font-medium text-destructive'>Delete Account</div>
<div className='text-sm text-muted-foreground'>Permanently delete your account</div>
</div>
</div>
</Button>
</Link>
</div>
</div>
</div>
</div>
)
redirect('/dashboard/account/billing')
}

View File

@@ -0,0 +1,24 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import EditProfileForm from '../../(components)/edit-profile-form'
export default function ProfilePage() {
return (
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your personal details</CardDescription>
</CardHeader>
<CardContent>
<EditProfileForm />
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import { AlertTriangleIcon } from 'lucide-react'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import ChangePasswordForm from '../../(components)/change-password-form'
import DeleteAccountForm from '../../(components)/delete-account-form'
// Security: password management, with account deletion in a clearly separated
// destructive card so the two actions can never be confused.
export default function SecurityPage() {
return (
<div className='max-w-2xl space-y-8'>
<Card>
<CardHeader>
<CardTitle>Password</CardTitle>
<CardDescription>
Change your password to keep your account secure
</CardDescription>
</CardHeader>
<CardContent>
<ChangePasswordForm />
</CardContent>
</Card>
<div className='space-y-3'>
<h3 className='text-sm font-semibold uppercase tracking-wide text-destructive'>
Danger zone
</h3>
<Card className='border-destructive/50'>
<CardHeader>
<div className='flex items-center gap-2 text-destructive'>
<AlertTriangleIcon className='h-5 w-5' />
<CardTitle>Delete Account</CardTitle>
</div>
<CardDescription>
Permanently delete your account and all associated data
</CardDescription>
</CardHeader>
<CardContent>
<DeleteAccountForm />
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import SupportForm from '../../(components)/support-form'
export default function SupportPage() {
return (
<div className='max-w-2xl'>
<Card>
<CardHeader>
<CardTitle>Contact Support</CardTitle>
<CardDescription>
Fill out the form below and we'll get back to you as soon as
possible.
</CardDescription>
</CardHeader>
<CardContent>
<SupportForm />
</CardContent>
</Card>
</div>
)
}

56
web/e2e/account.spec.ts Normal file
View File

@@ -0,0 +1,56 @@
import { expect, test } from '@playwright/test'
import { authenticate } from './session'
import { mockApi } from './mock-api'
test.describe('account settings (mocked API, no real backend)', () => {
test('/dashboard/account redirects to billing and shows the subscription', async ({
page,
context,
}) => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/account')
await expect(page).toHaveURL(/\/dashboard\/account\/billing/)
const nav = page.getByRole('navigation', { name: 'Section navigation' })
await expect(
nav.getByRole('link', { name: 'Billing & plan' })
).toHaveAttribute('aria-current', 'page')
// Mocked subscription plan name renders.
await expect(page.getByRole('heading', { name: 'Pro' })).toBeVisible()
})
test('security deep link survives refresh and separates the danger zone', async ({
page,
context,
}) => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/account/security')
const nav = page.getByRole('navigation', { name: 'Section navigation' })
await expect(nav.getByRole('link', { name: 'Security' })).toHaveAttribute(
'aria-current',
'page'
)
await expect(page.getByText('Password', { exact: true })).toBeVisible()
await expect(page.getByText('Danger zone')).toBeVisible()
await expect(
page.getByRole('heading', { name: 'Delete Account' })
).toBeVisible()
})
test('legacy routes redirect into the merged settings', async ({
page,
context,
}) => {
await authenticate(context)
await mockApi(page)
await page.goto('/dashboard/account/change-password')
await expect(page).toHaveURL(/\/dashboard\/account\/security/)
await page.goto('/dashboard/account/edit-profile')
await expect(page).toHaveURL(/\/dashboard\/account\/profile/)
})
})