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:
+42
-49
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+71
-81
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+45
-49
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+59
-64
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+46
-53
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+84
-86
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+60
-64
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+127
-127
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+31
-38
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user