mirror of
https://github.com/vernu/textbee.git
synced 2026-08-01 09:59:02 -04:00
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:
10
web/app/(app)/dashboard/account/billing/page.tsx
Normal file
10
web/app/(app)/dashboard/account/billing/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
36
web/app/(app)/dashboard/account/layout.tsx
Normal file
36
web/app/(app)/dashboard/account/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
24
web/app/(app)/dashboard/account/profile/page.tsx
Normal file
24
web/app/(app)/dashboard/account/profile/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
50
web/app/(app)/dashboard/account/security/page.tsx
Normal file
50
web/app/(app)/dashboard/account/security/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
27
web/app/(app)/dashboard/account/support/page.tsx
Normal file
27
web/app/(app)/dashboard/account/support/page.tsx
Normal 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
56
web/e2e/account.spec.ts
Normal 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/)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user