perf(web): improve dialog sizing and footer layout

- migrate frontend dialogs to the shared footer API so actions stay separated from scrollable body content.
- tune dialog dimensions for model analytics, prefill groups, billing history, channel model sync, and related workflows.
- update channel terminology and dialog action translations across supported locales.
This commit is contained in:
QuentinHsu
2026-06-06 21:49:33 +08:00
parent 7a5348caa3
commit 2eaa943d9f
80 changed files with 8137 additions and 8517 deletions
@@ -20,17 +20,10 @@ import { useEffect } from 'react'
import { RefreshCw, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import { useAccessToken } from '../../hooks'
// ============================================================================
@@ -57,45 +50,18 @@ export function AccessTokenDialog({
}, [open, token, generate])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Access Token')}</DialogTitle>
<DialogDescription>
{t(
"Your system access token for API authentication. Keep it secure and don't share it with others."
)}
</DialogDescription>
</DialogHeader>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label>
<div className='flex gap-2'>
<Input
id='token'
type='text'
value={token}
readOnly
className='font-mono text-xs'
placeholder={t('Click "Generate" to create a token')}
/>
<CopyButton
value={token}
variant='outline'
className='size-9'
iconClassName='size-4'
tooltip={t('Copy token')}
aria-label={t('Copy token')}
/>
</div>
<p className='text-muted-foreground text-xs'>
{t('Use this token for API authentication')}
</p>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Access Token')}
description={t(
"Your system access token for API authentication. Keep it secure and don't share it with others."
)}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
@@ -116,8 +82,35 @@ export function AccessTokenDialog({
)}
{generating ? t('Generating...') : t('Regenerate')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='token'>{t('Token')}</Label>
<div className='flex gap-2'>
<Input
id='token'
type='text'
value={token}
readOnly
className='font-mono text-xs'
placeholder={t('Click "Generate" to create a token')}
/>
<CopyButton
value={token}
variant='outline'
className='size-9'
iconClassName='size-4'
tooltip={t('Copy token')}
aria-label={t('Copy token')}
/>
</div>
<p className='text-muted-foreground text-xs'>
{t('Use this token for API authentication')}
</p>
</div>
</div>
</Dialog>
)
}
@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input'
import { updateUserProfile } from '../../api'
@@ -114,82 +107,79 @@ export function ChangePasswordDialog({
}
}
const formId = 'change-password-form'
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>{t('Change Password')}</DialogTitle>
<DialogDescription>
{t('Update your password for account:')}{' '}
<strong>{username}</strong>
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Change Password')}
description={
<>
{t('Update your password for account:')} <strong>{username}</strong>
</>
}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button type='submit' form={formId} disabled={loading}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Changing...') : t('Change Password')}
</Button>
</>
}
>
<form id={formId} onSubmit={handleSubmit} className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='currentPassword'>{t('Current Password')}</Label>
<PasswordInput
id='currentPassword'
value={formData.originalPassword}
onChange={(e) => handleChange('originalPassword', e.target.value)}
disabled={loading}
required
autoComplete='current-password'
/>
</div>
<div className='my-6 space-y-4'>
<div className='space-y-2'>
<Label htmlFor='currentPassword'>{t('Current Password')}</Label>
<PasswordInput
id='currentPassword'
value={formData.originalPassword}
onChange={(e) =>
handleChange('originalPassword', e.target.value)
}
disabled={loading}
required
autoComplete='current-password'
/>
</div>
<div className='space-y-2'>
<Label htmlFor='newPassword'>{t('New Password')}</Label>
<PasswordInput
id='newPassword'
value={formData.newPassword}
onChange={(e) => handleChange('newPassword', e.target.value)}
disabled={loading}
required
minLength={8}
autoComplete='new-password'
/>
<p className='text-muted-foreground text-xs'>
{t('Must be at least 8 characters')}
</p>
</div>
<div className='space-y-2'>
<Label htmlFor='newPassword'>{t('New Password')}</Label>
<PasswordInput
id='newPassword'
value={formData.newPassword}
onChange={(e) => handleChange('newPassword', e.target.value)}
disabled={loading}
required
minLength={8}
autoComplete='new-password'
/>
<p className='text-muted-foreground text-xs'>
{t('Must be at least 8 characters')}
</p>
</div>
<div className='space-y-2'>
<Label htmlFor='confirmPassword'>
{t('Confirm New Password')}
</Label>
<PasswordInput
id='confirmPassword'
value={formData.confirmPassword}
onChange={(e) =>
handleChange('confirmPassword', e.target.value)
}
disabled={loading}
required
autoComplete='new-password'
/>
</div>
</div>
<DialogFooter>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
disabled={loading}
>
{t('Cancel')}
</Button>
<Button type='submit' disabled={loading}>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Changing...') : t('Change Password')}
</Button>
</DialogFooter>
</form>
</DialogContent>
<div className='space-y-2'>
<Label htmlFor='confirmPassword'>{t('Confirm New Password')}</Label>
<PasswordInput
id='confirmPassword'
value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
disabled={loading}
required
autoComplete='new-password'
/>
</div>
</form>
</Dialog>
)
}
@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store'
import { api } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { deleteUserAccount } from '../../api'
// ============================================================================
@@ -101,45 +94,24 @@ export function DeleteAccountDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle className='text-destructive flex items-center gap-2'>
<AlertTriangle className='h-5 w-5' />
{t('Delete Account')}
</DialogTitle>
<DialogDescription>
{t(
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
)}
</DialogDescription>
</DialogHeader>
<div className='my-6 space-y-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: This action is permanent and irreversible!')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='confirmation'>
{t('Type')} <strong>{username}</strong> {t('to confirm')}
</Label>
<Input
id='confirmation'
type='text'
value={confirmation}
onChange={(e) => setConfirmation(e.target.value)}
disabled={loading}
placeholder={username}
autoComplete='off'
/>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={
<>
<AlertTriangle className='h-5 w-5' />
{t('Delete Account')}
</>
}
description={t(
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
)}
contentClassName='sm:max-w-md'
titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
@@ -157,8 +129,32 @@ export function DeleteAccountDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Deleting...') : t('Delete Account')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='my-6 space-y-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: This action is permanent and irreversible!')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='confirmation'>
{t('Type')} <strong>{username}</strong> {t('to confirm')}
</Label>
<Input
id='confirmation'
type='text'
value={confirmation}
onChange={(e) => setConfirmation(e.target.value)}
disabled={loading}
placeholder={username}
autoComplete='off'
/>
</div>
</div>
</Dialog>
)
}
@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useCountdown } from '@/hooks/use-countdown'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { sendEmailVerification, bindEmail } from '../../api'
// ============================================================================
@@ -129,60 +122,22 @@ export function EmailBindDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Bind Email')}</DialogTitle>
<DialogDescription>
{currentEmail
? t('Current email: {{email}}. Enter a new email to change.', {
email: currentEmail,
})
: t('Bind an email address to your account.')}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='email'>{t('Email Address')}</Label>
<Input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('Enter your email')}
disabled={loading}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<div className='flex gap-2'>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code')}
disabled={loading}
maxLength={6}
/>
<Button
type='button'
variant='outline'
onClick={handleSendCode}
disabled={sendingCode || isActive || !email}
>
{isActive
? `${secondsLeft}s`
: sendingCode
? t('Sending...')
: t('Send')}
</Button>
</div>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={t('Bind Email')}
description={
currentEmail
? t('Current email: {{email}}. Enter a new email to change.', {
email: currentEmail,
})
: t('Bind an email address to your account.')
}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
type='button'
variant='outline'
@@ -199,8 +154,48 @@ export function EmailBindDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Binding...') : t('Bind Email')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='email'>{t('Email Address')}</Label>
<Input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('Enter your email')}
disabled={loading}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<div className='flex gap-2'>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code')}
disabled={loading}
maxLength={6}
/>
<Button
type='button'
variant='outline'
onClick={handleSendCode}
disabled={sendingCode || isActive || !email}
>
{isActive
? `${secondsLeft}s`
: sendingCode
? t('Sending...')
: t('Send')}
</Button>
</div>
</div>
</div>
</Dialog>
)
}
@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { Send } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog } from '@/components/dialog'
// ============================================================================
// Telegram Bind Dialog Component
@@ -45,56 +39,55 @@ export function TelegramBindDialog({
}: TelegramBindDialogProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Bind Telegram Account')}</DialogTitle>
<DialogDescription>
{t('Click the button below to bind your Telegram account')}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Bind Telegram Account')}
description={t('Click the button below to bind your Telegram account')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
>
<div className='space-y-4 py-4'>
<Alert>
<Send className='h-4 w-4' />
<AlertDescription>
{t(
'You will be redirected to Telegram to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='space-y-4 py-4'>
<Alert>
<Send className='h-4 w-4' />
<AlertDescription>
{t(
'You will be redirected to Telegram to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='flex flex-col items-center justify-center gap-4 rounded-lg border p-6'>
<div className='flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 dark:bg-blue-900'>
<Send className='h-6 w-6 text-blue-600 dark:text-blue-400' />
</div>
<div className='text-center'>
<p className='text-muted-foreground text-sm'>
{t('Bot:')}{' '}
<span className='font-mono font-semibold'>@{botName}</span>
</p>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
"After clicking the button, you'll be asked to authorize the bot"
)}
</p>
</div>
{/* Telegram Login Widget will be injected here by react-telegram-login */}
<div id='telegram-login-widget' className='flex justify-center'>
{/* This would require the react-telegram-login library */}
<div className='text-muted-foreground rounded-lg border border-dashed px-6 py-3 text-sm'>
{t('Telegram Login Widget')}
</div>
</div>
<div className='flex flex-col items-center justify-center gap-4 rounded-lg border p-6'>
<div className='flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 dark:bg-blue-900'>
<Send className='h-6 w-6 text-blue-600 dark:text-blue-400' />
</div>
<p className='text-muted-foreground text-center text-xs'>
{t('The binding will complete automatically after authorization')}
</p>
<div className='text-center'>
<p className='text-muted-foreground text-sm'>
{t('Bot:')}{' '}
<span className='font-mono font-semibold'>@{botName}</span>
</p>
<p className='text-muted-foreground mt-1 text-xs'>
{t(
"After clicking the button, you'll be asked to authorize the bot"
)}
</p>
</div>
{/* Telegram Login Widget will be injected here by react-telegram-login */}
<div id='telegram-login-widget' className='flex justify-center'>
{/* This would require the react-telegram-login library */}
<div className='text-muted-foreground rounded-lg border border-dashed px-6 py-3 text-sm'>
{t('Telegram Login Widget')}
</div>
</div>
</div>
</DialogContent>
<p className='text-muted-foreground text-center text-xs'>
{t('The binding will complete automatically after authorization')}
</p>
</div>
</Dialog>
)
}
@@ -23,17 +23,10 @@ import { toast } from 'sonner'
import { regenerate2FABackupCodes } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
// ============================================================================
// Two-FA Backup Codes Dialog Component
@@ -94,82 +87,26 @@ export function TwoFABackupDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle className='flex items-center gap-2'>
<RefreshCw className='h-5 w-5' />
{t('Regenerate Backup Codes')}
</DialogTitle>
<DialogDescription>
{backupCodes.length > 0
? t('Your new backup codes are ready')
: t('Generate new backup codes for account recovery')}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
{backupCodes.length === 0 ? (
<>
<Alert>
<AlertDescription>
{t(
'Generating new codes will invalidate all existing backup codes.'
)}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter authenticator code')}
maxLength={6}
disabled={loading}
/>
</div>
</>
) : (
<>
<Alert>
<AlertDescription>
{t(
'Save these codes in a safe place. Each code can only be used once.'
)}
</AlertDescription>
</Alert>
<div className='rounded-lg border p-4'>
<div className='grid grid-cols-2 gap-2'>
{backupCodes.map((code, index) => (
<div
key={index}
className='bg-muted rounded-md p-2 text-center font-mono text-sm'
>
{code}
</div>
))}
</div>
</div>
<CopyButton
value={backupCodes.join('\n')}
variant='outline'
size='default'
className='w-full'
iconClassName='mr-2 size-4'
tooltip={t('Copy all backup codes')}
aria-label={t('Copy all backup codes')}
>
{t('Copy All Codes')}
</CopyButton>
</>
)}
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={
<>
<RefreshCw className='h-5 w-5' />
{t('Regenerate Backup Codes')}
</>
}
description={
backupCodes.length > 0
? t('Your new backup codes are ready')
: t('Generate new backup codes for account recovery')
}
contentClassName='sm:max-w-md'
titleClassName='flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
{backupCodes.length === 0 ? (
<>
<Button
@@ -187,8 +124,69 @@ export function TwoFABackupDialog({
) : (
<Button onClick={handleDone}>{t('Done')}</Button>
)}
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
{backupCodes.length === 0 ? (
<>
<Alert>
<AlertDescription>
{t(
'Generating new codes will invalidate all existing backup codes.'
)}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter authenticator code')}
maxLength={6}
disabled={loading}
/>
</div>
</>
) : (
<>
<Alert>
<AlertDescription>
{t(
'Save these codes in a safe place. Each code can only be used once.'
)}
</AlertDescription>
</Alert>
<div className='rounded-lg border p-4'>
<div className='grid grid-cols-2 gap-2'>
{backupCodes.map((code, index) => (
<div
key={index}
className='bg-muted rounded-md p-2 text-center font-mono text-sm'
>
{code}
</div>
))}
</div>
</div>
<CopyButton
value={backupCodes.join('\n')}
variant='outline'
size='default'
className='w-full'
iconClassName='mr-2 size-4'
tooltip={t('Copy all backup codes')}
aria-label={t('Copy all backup codes')}
>
{t('Copy All Codes')}
</CopyButton>
</>
)}
</div>
</Dialog>
)
}
@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
// ============================================================================
// Two-FA Disable Dialog Component
@@ -98,60 +91,24 @@ export function TwoFADisableDialog({
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle className='text-destructive flex items-center gap-2'>
<AlertTriangle className='h-5 w-5' />
{t('Disable Two-Factor Authentication')}
</DialogTitle>
<DialogDescription>
{t(
'This action will permanently remove 2FA protection from your account.'
)}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: Disabling 2FA will make your account less secure.')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code or backup code')}
disabled={loading}
/>
<p className='text-muted-foreground text-xs'>
{t('Enter your authenticator code or a backup code')}
</p>
</div>
<div className='flex items-start space-x-2'>
<Checkbox
id='confirm'
checked={confirmed}
onCheckedChange={(checked) => setConfirmed(checked as boolean)}
/>
<Label
htmlFor='confirm'
className='text-sm leading-tight font-normal'
>
{t(
'I understand that disabling 2FA will remove all protection and backup codes'
)}
</Label>
</div>
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={
<>
<AlertTriangle className='h-5 w-5' />
{t('Disable Two-Factor Authentication')}
</>
}
description={t(
'This action will permanently remove 2FA protection from your account.'
)}
contentClassName='sm:max-w-md'
titleClassName='text-destructive flex items-center gap-2'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
<Button
variant='outline'
onClick={() => handleOpenChange(false)}
@@ -167,8 +124,47 @@ export function TwoFADisableDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Disabling...') : t('Disable 2FA')}
</Button>
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>
{t('Warning: Disabling 2FA will make your account less secure.')}
</AlertDescription>
</Alert>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter code or backup code')}
disabled={loading}
/>
<p className='text-muted-foreground text-xs'>
{t('Enter your authenticator code or a backup code')}
</p>
</div>
<div className='flex items-start space-x-2'>
<Checkbox
id='confirm'
checked={confirmed}
onCheckedChange={(checked) => setConfirmed(checked as boolean)}
/>
<Label
htmlFor='confirm'
className='text-sm leading-tight font-normal'
>
{t(
'I understand that disabling 2FA will remove all protection and backup codes'
)}
</Label>
</div>
</div>
</Dialog>
)
}
@@ -24,17 +24,10 @@ import { toast } from 'sonner'
import { setup2FA, enable2FA } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import type { TwoFASetupData } from '../../types'
// ============================================================================
@@ -136,123 +129,23 @@ export function TwoFASetupDialog({
}, [open, setupData, initializing, handleSetup])
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-lg'>
<DialogHeader>
<DialogTitle>{t('Setup Two-Factor Authentication')}</DialogTitle>
<DialogDescription>
{t('Step')} {step + 1} {t('of 3:')} {stepLabels[step]}
</DialogDescription>
</DialogHeader>
<div className='space-y-4 py-4'>
{initializing ? (
<div className='flex flex-col items-center justify-center gap-3 py-8'>
<div className='border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent' />
<div className='text-muted-foreground text-sm'>
{t('Setting up 2FA...')}
</div>
</div>
) : !setupData ? (
<div className='flex justify-center py-8'>
<div className='text-muted-foreground'>
{t('Failed to load setup data')}
</div>
</div>
) : (
<>
{/* Step 0: QR Code */}
{step === 0 && (
<div className='space-y-4'>
<p className='text-muted-foreground text-sm'>
{t(
'Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)'
)}
</p>
<div className='flex justify-center rounded-lg bg-white p-4'>
<QRCodeSVG value={setupData.qr_code_data} size={200} />
</div>
<div className='bg-muted rounded-lg p-3'>
<div className='flex items-center justify-between'>
<div>
<p className='text-muted-foreground text-xs'>
{t('Or enter this key manually:')}
</p>
<code className='font-mono text-sm'>
{setupData.secret}
</code>
</div>
<CopyButton
value={setupData.secret}
variant='ghost'
tooltip={t('Copy secret key')}
aria-label={t('Copy secret key')}
/>
</div>
</div>
</div>
)}
{/* Step 1: Backup Codes */}
{step === 1 && (
<div className='space-y-4'>
<Alert>
<AlertDescription>
{t(
'Save these backup codes in a safe place. Each code can only be used once.'
)}
</AlertDescription>
</Alert>
<div className='rounded-lg border p-4'>
<div className='grid grid-cols-2 gap-2'>
{setupData.backup_codes.map((code, index) => (
<div
key={index}
className='bg-muted rounded-md p-2 text-center font-mono text-sm'
>
{code}
</div>
))}
</div>
</div>
<CopyButton
value={setupData.backup_codes.join('\n')}
variant='outline'
size='default'
className='w-full'
iconClassName='mr-2 size-4'
tooltip={t('Copy all backup codes')}
aria-label={t('Copy all backup codes')}
>
{t('Copy All Codes')}
</CopyButton>
</div>
)}
{/* Step 2: Verify */}
{step === 2 && (
<div className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter 6-digit code')}
maxLength={6}
disabled={loading}
/>
<p className='text-muted-foreground text-xs'>
{t('Enter the 6-digit code from your authenticator app')}
</p>
</div>
</div>
)}
</>
)}
</div>
<DialogFooter>
<Dialog
open={open}
onOpenChange={handleOpenChange}
title={t('Setup Two-Factor Authentication')}
description={
<>
{t('Step')}
{step + 1}
{t('of 3:')}
{stepLabels[step]}
</>
}
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
footer={
<>
{step > 0 && (
<Button
variant='outline'
@@ -278,8 +171,115 @@ export function TwoFASetupDialog({
{loading ? t('Enabling...') : t('Enable 2FA')}
</Button>
)}
</DialogFooter>
</DialogContent>
</>
}
>
<div className='space-y-4 py-4'>
{initializing ? (
<div className='flex flex-col items-center justify-center gap-3 py-8'>
<div className='border-primary h-8 w-8 animate-spin rounded-full border-4 border-t-transparent' />
<div className='text-muted-foreground text-sm'>
{t('Setting up 2FA...')}
</div>
</div>
) : !setupData ? (
<div className='flex justify-center py-8'>
<div className='text-muted-foreground'>
{t('Failed to load setup data')}
</div>
</div>
) : (
<>
{/* Step 0: QR Code */}
{step === 0 && (
<div className='space-y-4'>
<p className='text-muted-foreground text-sm'>
{t(
'Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)'
)}
</p>
<div className='flex justify-center rounded-lg bg-white p-4'>
<QRCodeSVG value={setupData.qr_code_data} size={200} />
</div>
<div className='bg-muted rounded-lg p-3'>
<div className='flex items-center justify-between'>
<div>
<p className='text-muted-foreground text-xs'>
{t('Or enter this key manually:')}
</p>
<code className='font-mono text-sm'>
{setupData.secret}
</code>
</div>
<CopyButton
value={setupData.secret}
variant='ghost'
tooltip={t('Copy secret key')}
aria-label={t('Copy secret key')}
/>
</div>
</div>
</div>
)}
{/* Step 1: Backup Codes */}
{step === 1 && (
<div className='space-y-4'>
<Alert>
<AlertDescription>
{t(
'Save these backup codes in a safe place. Each code can only be used once.'
)}
</AlertDescription>
</Alert>
<div className='rounded-lg border p-4'>
<div className='grid grid-cols-2 gap-2'>
{setupData.backup_codes.map((code, index) => (
<div
key={index}
className='bg-muted rounded-md p-2 text-center font-mono text-sm'
>
{code}
</div>
))}
</div>
</div>
<CopyButton
value={setupData.backup_codes.join('\n')}
variant='outline'
size='default'
className='w-full'
iconClassName='mr-2 size-4'
tooltip={t('Copy all backup codes')}
aria-label={t('Copy all backup codes')}
>
{t('Copy All Codes')}
</CopyButton>
</div>
)}
{/* Step 2: Verify */}
{step === 2 && (
<div className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='code'>{t('Verification Code')}</Label>
<Input
id='code'
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder={t('Enter 6-digit code')}
maxLength={6}
disabled={loading}
/>
<p className='text-muted-foreground text-xs'>
{t('Enter the 6-digit code from your authenticator app')}
</p>
</div>
</div>
)}
</>
)}
</div>
</Dialog>
)
}
@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { QrCode } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Dialog } from '@/components/dialog'
// ============================================================================
// WeChat Bind Dialog Component
@@ -43,40 +37,39 @@ export function WeChatBindDialog({
}: WeChatBindDialogProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className='sm:max-w-md'>
<DialogHeader>
<DialogTitle>{t('Bind WeChat Account')}</DialogTitle>
<DialogDescription>
{t('Scan the QR code with WeChat to bind your account')}
</DialogDescription>
</DialogHeader>
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Bind WeChat Account')}
description={t('Scan the QR code with WeChat to bind your account')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
>
<div className='space-y-4 py-4'>
<Alert>
<QrCode className='h-4 w-4' />
<AlertDescription>
{t(
'Please use WeChat\'s "Scan QR Code" feature to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='space-y-4 py-4'>
<Alert>
<QrCode className='h-4 w-4' />
<AlertDescription>
{t(
'Please use WeChat\'s "Scan QR Code" feature to complete the binding process.'
)}
</AlertDescription>
</Alert>
<div className='flex flex-col items-center justify-center rounded-lg border border-dashed p-8'>
<QrCode className='text-muted-foreground mb-3 h-16 w-16' />
<p className='text-muted-foreground text-sm'>
{t('WeChat QR code will be displayed here')}
</p>
<p className='text-muted-foreground mt-2 text-xs'>
{t('This feature requires server-side WeChat configuration')}
</p>
</div>
<p className='text-muted-foreground text-center text-xs'>
{t('After scanning, the binding will complete automatically')}
<div className='flex flex-col items-center justify-center rounded-lg border border-dashed p-8'>
<QrCode className='text-muted-foreground mb-3 h-16 w-16' />
<p className='text-muted-foreground text-sm'>
{t('WeChat QR code will be displayed here')}
</p>
<p className='text-muted-foreground mt-2 text-xs'>
{t('This feature requires server-side WeChat configuration')}
</p>
</div>
</DialogContent>
<p className='text-muted-foreground text-center text-xs'>
{t('After scanning, the binding will complete automatically')}
</p>
</div>
</Dialog>
)
}