Commit 15ff8e02 by 同語 Committed by GitHub

chore(web): improve frontend dialog layout and sizing (#5346)

Merge pull request #5346 from QuantumNous/perf/ui-dialog
parents a1c82841 2eaa943d
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { cn } from '@/lib/utils'
import {
Dialog as DialogRoot,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
type DialogProps = React.ComponentProps<typeof DialogRoot> & {
title: React.ReactNode
description?: React.ReactNode
children: React.ReactNode
trigger?: React.ReactElement
footer?: React.ReactNode
contentHeight?: React.CSSProperties['height']
contentClassName?: string
headerClassName?: string
titleClassName?: string
descriptionClassName?: string
bodyClassName?: string
footerClassName?: string
initialFocus?: boolean
showCloseButton?: boolean
}
const dialogContentMotionClassName =
'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 duration-100'
export function Dialog({
title,
description,
children,
trigger,
footer,
contentHeight = 'auto',
contentClassName,
headerClassName,
titleClassName,
descriptionClassName,
bodyClassName,
footerClassName,
initialFocus,
showCloseButton,
...dialogProps
}: DialogProps) {
return (
<DialogRoot {...dialogProps}>
{trigger ? <DialogTrigger render={trigger} /> : null}
<DialogContent
className={cn(
'flex max-h-[calc(100vh-2rem)] w-full flex-col gap-4 overflow-hidden p-4 sm:max-w-2xl sm:p-6',
contentClassName,
dialogContentMotionClassName
)}
initialFocus={initialFocus}
showCloseButton={showCloseButton}
style={
{
'--dialog-content-height': contentHeight,
} as React.CSSProperties
}
>
<DialogHeader
className={cn('flex-shrink-0 text-start', headerClassName)}
>
<DialogTitle className={titleClassName}>{title}</DialogTitle>
{description ? (
<DialogDescription className={descriptionClassName}>
{description}
</DialogDescription>
) : null}
</DialogHeader>
<div
className={cn(
'-mx-1 min-h-0 overflow-x-hidden overflow-y-auto overscroll-contain',
'h-[var(--dialog-content-height)] max-h-[calc(100vh-14rem)]'
)}
>
<div
className={cn(
'min-w-0 px-1 py-1',
'[&_form]:overflow-x-visible',
'[&_[data-slot=scroll-area-viewport]]:px-1 [&_[data-slot=scroll-area-viewport]]:py-1',
bodyClassName
)}
>
{children}
</div>
</div>
{footer ? (
<DialogFooter
className={cn(
'flex-shrink-0 gap-2 sm:-mx-6 sm:-mb-6 sm:justify-end sm:p-6',
footerClassName
)}
>
{footer}
</DialogFooter>
) : null}
</DialogContent>
</DialogRoot>
)
}
...@@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications' ...@@ -25,15 +25,8 @@ import { useNotifications } from '@/hooks/use-notifications'
import { useSystemConfig } from '@/hooks/use-system-config' import { useSystemConfig } from '@/hooks/use-system-config'
import { useTopNavLinks } from '@/hooks/use-top-nav-links' import { useTopNavLinks } from '@/hooks/use-top-nav-links'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Dialog } from '@/components/dialog'
import { LanguageSwitcher } from '@/components/language-switcher' import { LanguageSwitcher } from '@/components/language-switcher'
import { NotificationPopover } from '@/components/notification-popover' import { NotificationPopover } from '@/components/notification-popover'
import { ProfileDropdown } from '@/components/profile-dropdown' import { ProfileDropdown } from '@/components/profile-dropdown'
...@@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) { ...@@ -427,28 +420,26 @@ export function PublicHeader(props: PublicHeaderProps) {
closeAuthPrompt() closeAuthPrompt()
} }
}} }}
> title={t('Sign in required')}
<DialogContent className='sm:max-w-md'> description={t('Please sign in to view {{module}}.', {
<DialogHeader> module: authPromptTarget?.title || '',
<DialogTitle>{t('Sign in required')}</DialogTitle> })}
<DialogDescription> contentClassName='sm:max-w-md'
{t('Please sign in to view {{module}}.', { contentHeight='auto'
module: authPromptTarget?.title || '', footer={
})} <>
</DialogDescription>
</DialogHeader>
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
{t('Redirecting to sign in in {{seconds}} seconds.', {
seconds: authPromptSecondsLeft,
})}
</div>
<DialogFooter>
<Button variant='outline' onClick={closeAuthPrompt}> <Button variant='outline' onClick={closeAuthPrompt}>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<Button onClick={navigateToSignIn}>{t('Sign in now')}</Button> <Button onClick={navigateToSignIn}>{t('Sign in now')}</Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='bg-muted/40 text-muted-foreground rounded-lg px-3 py-2 text-sm'>
{t('Redirecting to sign in in {{seconds}} seconds.', {
seconds: authPromptSecondsLeft,
})}
</div>
</Dialog> </Dialog>
</> </>
) )
......
...@@ -20,16 +20,9 @@ import { useMemo } from 'react' ...@@ -20,16 +20,9 @@ import { useMemo } from 'react'
import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react' import { ShieldCheck, KeyRound, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Dialog } from '@/components/dialog'
import type { import type {
SecureVerificationState, SecureVerificationState,
VerificationMethod, VerificationMethod,
...@@ -91,122 +84,118 @@ export function SecureVerificationDialog({ ...@@ -91,122 +84,118 @@ export function SecureVerificationDialog({
(activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6)) (activeMethod === '2fa' && (!state.code.trim() || state.code.length < 6))
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent open={open}
className='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 gap-0 overflow-hidden border-none p-0 shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl' onOpenChange={onOpenChange}
showCloseButton={!state.loading} title={
> <>
<div className='bg-background flex max-h-[calc(100dvh-2rem)] flex-col'> <ShieldCheck className='text-primary h-5 w-5' />
<DialogHeader className='border-b px-6 py-5 text-left'> {title}
<DialogTitle className='flex items-center gap-2 text-lg font-semibold'> </>
<ShieldCheck className='text-primary h-5 w-5' /> }
{title} description={description}
</DialogTitle> contentClassName='top-[8vh] max-w-[calc(100%-1.5rem)] translate-y-0 overflow-hidden border-none shadow-xl sm:top-1/2 sm:max-w-md sm:translate-y-[-50%] sm:rounded-xl'
<DialogDescription className='text-left'> headerClassName='border-b pb-4 text-left'
{description} titleClassName='flex items-center gap-2 text-lg font-semibold'
</DialogDescription> descriptionClassName='text-left'
</DialogHeader> contentHeight='auto'
bodyClassName='px-1 py-1'
<div className='flex-1 overflow-y-auto px-6 py-5'> showCloseButton={!state.loading}
{availableTabs.length === 0 ? ( footerClassName='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'
<div className='grid place-items-center gap-4 text-center'> footer={
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'> <>
<ShieldCheck className='text-muted-foreground h-8 w-8' /> <Button
</div> type='button'
<p className='text-muted-foreground text-sm'> variant='outline'
{t( disabled={state.loading}
'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.' onClick={onCancel}
)} >
</p> {t('Cancel')}
</div> </Button>
) : ( <Button
<Tabs type='button'
value={activeMethod ?? availableTabs[0]} onClick={handleVerify}
onValueChange={(value) => disabled={availableTabs.length === 0 || verifyDisabled}
onMethodChange(value as VerificationMethod) >
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</>
}
>
{availableTabs.length === 0 ? (
<div className='grid place-items-center gap-4 text-center'>
<div className='bg-muted flex h-16 w-16 items-center justify-center rounded-2xl'>
<ShieldCheck className='text-muted-foreground h-8 w-8' />
</div>
<p className='text-muted-foreground text-sm'>
{t(
'Enable Two-factor Authentication or Passkey in your profile to unlock sensitive operations.'
)}
</p>
</div>
) : (
<Tabs
value={activeMethod ?? availableTabs[0]}
onValueChange={(value) => onMethodChange(value as VerificationMethod)}
className='gap-4'
>
<TabsList>
{methods.has2FA && (
<TabsTrigger value='2fa'>{t('Authenticator code')}</TabsTrigger>
)}
{methods.hasPasskey && methods.passkeySupported && (
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger>
)}
</TabsList>
<TabsContent value='2fa' className='space-y-3'>
<p className='text-muted-foreground text-sm'>
{t(
'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.'
)}
</p>
<Input
inputMode='numeric'
maxLength={8}
value={state.code}
onChange={(event) => onCodeChange(event.target.value)}
placeholder={t('Enter verification code')}
disabled={state.loading}
autoFocus={activeMethod === '2fa'}
onKeyDown={(event) => {
if (event.key === 'Enter' && !verifyDisabled) {
event.preventDefault()
handleVerify()
} }
className='gap-4' }}
> />
<TabsList> </TabsContent>
{methods.has2FA && (
<TabsTrigger value='2fa'> <TabsContent value='passkey' className='space-y-4'>
{t('Authenticator code')} <div className='bg-muted/50 flex items-center justify-center rounded-lg p-4'>
</TabsTrigger> <div className='text-muted-foreground flex items-center gap-3'>
)} <KeyRound className='text-primary h-6 w-6' />
{methods.hasPasskey && methods.passkeySupported && ( <div className='text-left text-sm'>
<TabsTrigger value='passkey'>{t('Passkey')}</TabsTrigger> <p className='text-foreground font-medium'>
)} {t('Use your Passkey')}
</TabsList> </p>
<p>
<TabsContent value='2fa' className='space-y-3'>
<p className='text-muted-foreground text-sm'>
{t( {t(
'Enter the 6-digit Time-based One-Time Password or 8-character backup code from your authenticator app.' 'We will prompt your device to confirm using biometrics or your hardware key.'
)} )}
</p> </p>
<Input </div>
inputMode='numeric' </div>
maxLength={8} </div>
value={state.code} {!methods.passkeySupported && (
onChange={(event) => onCodeChange(event.target.value)} <p className='text-destructive text-sm'>
placeholder={t('Enter verification code')} {t('This device does not support Passkey verification.')}
disabled={state.loading} </p>
autoFocus={activeMethod === '2fa'}
onKeyDown={(event) => {
if (event.key === 'Enter' && !verifyDisabled) {
event.preventDefault()
handleVerify()
}
}}
/>
</TabsContent>
<TabsContent value='passkey' className='space-y-4'>
<div className='bg-muted/50 flex items-center justify-center rounded-lg p-4'>
<div className='text-muted-foreground flex items-center gap-3'>
<KeyRound className='text-primary h-6 w-6' />
<div className='text-left text-sm'>
<p className='text-foreground font-medium'>
{t('Use your Passkey')}
</p>
<p>
{t(
'We will prompt your device to confirm using biometrics or your hardware key.'
)}
</p>
</div>
</div>
</div>
{!methods.passkeySupported && (
<p className='text-destructive text-sm'>
{t('This device does not support Passkey verification.')}
</p>
)}
</TabsContent>
</Tabs>
)} )}
</div> </TabsContent>
</Tabs>
<DialogFooter className='bg-muted/30 border-t px-6 py-4 sm:flex-row sm:justify-end'> )}
<Button
type='button'
variant='outline'
disabled={state.loading}
onClick={onCancel}
>
{t('Cancel')}
</Button>
<Button
type='button'
onClick={handleVerify}
disabled={availableTabs.length === 0 || verifyDisabled}
>
{state.loading && <Loader2 className='h-4 w-4 animate-spin' />}
{t('Verify')}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -33,14 +33,6 @@ import { cn } from '@/lib/utils' ...@@ -33,14 +33,6 @@ import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status' import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
...@@ -50,6 +42,7 @@ import { ...@@ -50,6 +42,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile' import { Turnstile } from '@/components/turnstile'
import { login, wechatLoginByCode } from '@/features/auth/api' import { login, wechatLoginByCode } from '@/features/auth/api'
...@@ -414,43 +407,16 @@ export function UserAuthForm({ ...@@ -414,43 +407,16 @@ export function UserAuthForm({
<Dialog <Dialog
open={isWeChatDialogOpen} open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange} onOpenChange={handleWeChatDialogChange}
> title={t('WeChat sign in')}
<DialogContent className='max-w-sm'> description={t(
<DialogHeader className='text-left'> 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
<DialogTitle>{t('WeChat sign in')}</DialogTitle> )}
<DialogDescription> contentClassName='max-w-sm'
{t( headerClassName='text-left'
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' contentHeight='auto'
)} bodyClassName='space-y-4'
</DialogDescription> footer={
</DialogHeader> <>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -474,8 +440,32 @@ export function UserAuthForm({ ...@@ -474,8 +440,32 @@ export function UserAuthForm({
) : null} ) : null}
{t('Confirm')} {t('Confirm')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog> </Dialog>
)} )}
</Form> </Form>
......
...@@ -27,14 +27,6 @@ import { cn } from '@/lib/utils' ...@@ -27,14 +27,6 @@ import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status' import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormField, FormField,
...@@ -44,6 +36,7 @@ import { ...@@ -44,6 +36,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { Turnstile } from '@/components/turnstile' import { Turnstile } from '@/components/turnstile'
import { register, wechatLoginByCode } from '@/features/auth/api' import { register, wechatLoginByCode } from '@/features/auth/api'
...@@ -387,43 +380,16 @@ export function SignUpForm({ ...@@ -387,43 +380,16 @@ export function SignUpForm({
<Dialog <Dialog
open={isWeChatDialogOpen} open={isWeChatDialogOpen}
onOpenChange={handleWeChatDialogChange} onOpenChange={handleWeChatDialogChange}
> title={t('WeChat sign in')}
<DialogContent className='max-w-sm'> description={t(
<DialogHeader className='text-left'> 'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.'
<DialogTitle>{t('WeChat sign in')}</DialogTitle> )}
<DialogDescription> contentClassName='max-w-sm'
{t( headerClassName='text-left'
'Scan the QR code to follow the official account and reply with “验证码” to receive your verification code.' contentHeight='auto'
)} bodyClassName='space-y-4'
</DialogDescription> footer={
</DialogHeader> <>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
<DialogFooter>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -447,8 +413,32 @@ export function SignUpForm({ ...@@ -447,8 +413,32 @@ export function SignUpForm({
) : null} ) : null}
{t('Confirm')} {t('Confirm')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{wechatQrCodeUrl ? (
<div className='flex justify-center'>
<img
src={wechatQrCodeUrl}
alt={t('WeChat login QR code')}
className='h-40 w-40 rounded-md border object-contain'
/>
</div>
) : (
<p className='text-muted-foreground text-sm'>
{t('QR code is not configured. Please contact support.')}
</p>
)}
<div className='grid gap-2'>
<Label htmlFor='wechat-code'>{t('Verification code')}</Label>
<Input
id='wechat-code'
placeholder={t('Enter the verification code')}
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
autoComplete='one-time-code'
/>
</div>
</Dialog> </Dialog>
)} )}
</Form> </Form>
......
...@@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table' ...@@ -22,14 +22,6 @@ import { type Table } from '@tanstack/react-table'
import { Power, PowerOff, Tag, Trash2 } from 'lucide-react' import { Power, PowerOff, Tag, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
...@@ -38,6 +30,7 @@ import { ...@@ -38,6 +30,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import { import {
handleBatchDelete, handleBatchDelete,
handleBatchDisable, handleBatchDisable,
...@@ -188,29 +181,21 @@ export function DataTableBulkActions<TData>({ ...@@ -188,29 +181,21 @@ export function DataTableBulkActions<TData>({
</BulkActionsToolbar> </BulkActionsToolbar>
{/* Set Tag Dialog */} {/* Set Tag Dialog */}
<Dialog open={showTagDialog} onOpenChange={setShowTagDialog}> <Dialog
<DialogContent> open={showTagDialog}
<DialogHeader> onOpenChange={setShowTagDialog}
<DialogTitle>{t('Set Tag')}</DialogTitle> title={t('Set Tag')}
<DialogDescription> description={
{t('Set a tag for')} {selectedIds.length}{' '} <>
{t('selected channel(s). Leave empty to remove tag.')} {t('Set a tag for')}
</DialogDescription> {selectedIds.length}{' '}
</DialogHeader> {t('selected channel(s). Leave empty to remove tag.')}
</>
<div className='grid gap-4 py-4'> }
<div className='grid gap-2'> contentHeight='auto'
<Label htmlFor='tag'>{t('Tag')}</Label> bodyClassName='space-y-4'
<Input footer={
id='tag' <>
placeholder={t('Enter tag name (optional)')}
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button <Button
variant='outline' variant='outline'
onClick={() => { onClick={() => {
...@@ -221,22 +206,37 @@ export function DataTableBulkActions<TData>({ ...@@ -221,22 +206,37 @@ export function DataTableBulkActions<TData>({
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<Button onClick={handleSetTag}>{t('Set Tag')}</Button> <Button onClick={handleSetTag}>{t('Set Tag')}</Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label htmlFor='tag'>{t('Tag')}</Label>
<Input
id='tag'
placeholder={t('Enter tag name (optional)')}
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</div>
</div>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> <Dialog
<DialogContent> open={showDeleteConfirm}
<DialogHeader> onOpenChange={setShowDeleteConfirm}
<DialogTitle>{t('Delete Channels?')}</DialogTitle> title={t('Delete Channels?')}
<DialogDescription> description={
{t('Are you sure you want to delete')} {selectedIds.length}{' '} <>
{t('channel(s)? This action cannot be undone.')} {t('Are you sure you want to delete')}
</DialogDescription> {selectedIds.length}{' '}
</DialogHeader> {t('channel(s)? This action cannot be undone.')}
</>
<DialogFooter> }
contentHeight='auto'
footer={
<>
<Button <Button
variant='outline' variant='outline'
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
...@@ -246,8 +246,10 @@ export function DataTableBulkActions<TData>({ ...@@ -246,8 +246,10 @@ export function DataTableBulkActions<TData>({
<Button variant='destructive' onClick={handleDeleteAll}> <Button variant='destructive' onClick={handleDeleteAll}>
{t('Delete')} {t('Delete')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{' '}
</Dialog> </Dialog>
</> </>
) )
......
...@@ -24,14 +24,7 @@ import { toast } from 'sonner' ...@@ -24,14 +24,7 @@ import { toast } from 'sonner'
import { formatCurrencyFromUSD } from '@/lib/currency' import { formatCurrencyFromUSD } from '@/lib/currency'
import { formatTimestampToDate } from '@/lib/format' import { formatTimestampToDate } from '@/lib/format'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import { Dialog } from '@/components/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { getCodexUsage, updateChannelBalance } from '../../api' import { getCodexUsage, updateChannelBalance } from '../../api'
import { channelsQueryKeys } from '../../lib' import { channelsQueryKeys } from '../../lib'
import { useChannels } from '../channels-provider' import { useChannels } from '../channels-provider'
...@@ -161,53 +154,55 @@ export function BalanceQueryDialog({ ...@@ -161,53 +154,55 @@ export function BalanceQueryDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle>{t('Query Balance')}</DialogTitle> title={t('Query Balance')}
<DialogDescription> description={
{t('Update balance for:')} <strong>{currentRow.name}</strong> <>
</DialogDescription> {t('Update balance for:')}
</DialogHeader> <strong>{currentRow.name}</strong>
</>
<div className='space-y-4 py-4'> }
{/* Current Balance Display */} contentHeight='auto'
<div className='bg-muted/50 rounded-lg border p-4'> bodyClassName='space-y-4'
<div className='text-muted-foreground mb-2 flex items-center gap-2 text-sm'> footer={
<DollarSign className='h-4 w-4' /> <>
<span>{t('Current Balance')}</span>
</div>
<div className='text-2xl font-bold'>
{balance !== null
? formatBalance(balance)
: formatBalance(currentRow.balance)}
</div>
<div className='text-muted-foreground mt-2 text-xs'>
{t('Last updated:')}{' '}
{formatDate(
balanceUpdatedTime ?? currentRow.balance_updated_time
)}
</div>
</div>
{/* Balance Update Button */}
<Button
className='w-full'
onClick={handleQueryBalance}
disabled={isQuerying}
>
{isQuerying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{!isQuerying && <RefreshCw className='mr-2 h-4 w-4' />}
{isQuerying ? t('Querying...') : t('Update Balance')}
</Button>
</div>
<DialogFooter>
<Button variant='outline' onClick={handleClose} disabled={isQuerying}> <Button variant='outline' onClick={handleClose} disabled={isQuerying}>
{t('Close')} {t('Close')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-4 py-4'>
{/* Current Balance Display */}
<div className='bg-muted/50 rounded-lg border p-4'>
<div className='text-muted-foreground mb-2 flex items-center gap-2 text-sm'>
<DollarSign className='h-4 w-4' />
<span>{t('Current Balance')}</span>
</div>
<div className='text-2xl font-bold'>
{balance !== null
? formatBalance(balance)
: formatBalance(currentRow.balance)}
</div>
<div className='text-muted-foreground mt-2 text-xs'>
{t('Last updated:')}{' '}
{formatDate(balanceUpdatedTime ?? currentRow.balance_updated_time)}
</div>
</div>
{/* Balance Update Button */}
<Button
className='w-full'
onClick={handleQueryBalance}
disabled={isQuerying}
>
{isQuerying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{!isQuerying && <RefreshCw className='mr-2 h-4 w-4' />}
{isQuerying ? t('Querying...') : t('Update Balance')}
</Button>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -24,15 +24,8 @@ import { tryPrettyJson } from '@/lib/utils' ...@@ -24,15 +24,8 @@ import { tryPrettyJson } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { completeCodexOAuth, startCodexOAuth } from '../../api' import { completeCodexOAuth, startCodexOAuth } from '../../api'
type CodexOAuthDialogProps = { type CodexOAuthDialogProps = {
...@@ -129,78 +122,18 @@ export function CodexOAuthDialog({ ...@@ -129,78 +122,18 @@ export function CodexOAuthDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-2xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Codex Authorization')}</DialogTitle> title={t('Codex Authorization')}
<DialogDescription> description={t(
{t( 'Generate a Codex OAuth credential and paste it into the channel key field.'
'Generate a Codex OAuth credential and paste it into the channel key field.' )}
)} contentClassName='sm:max-w-2xl'
</DialogDescription> contentHeight='auto'
</DialogHeader> bodyClassName='space-y-4'
footer={
<div className='space-y-4'> <>
<Alert>
<AlertDescription>
{t(
'1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".'
)}
</AlertDescription>
</Alert>
<div className='flex flex-wrap gap-2'>
<Button onClick={handleStart} disabled={state.isStarting}>
{state.isStarting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : (
<ExternalLink className='mr-2 h-4 w-4' />
)}
{t('Open authorization page')}
</Button>
<Button
type='button'
variant='outline'
disabled={!canCopyAuthorizeUrl}
onClick={async () => {
if (!state.authorizeUrl) return
await copyToClipboard(state.authorizeUrl)
}}
aria-label={t('Copy authorization link')}
title={t('Copy authorization link')}
>
{copiedText === state.authorizeUrl ? (
<Check className='mr-2 h-4 w-4 text-green-600' />
) : (
<Copy className='mr-2 h-4 w-4' />
)}
{t('Copy authorization link')}
</Button>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Callback URL')}</div>
<Input
value={state.callbackUrl}
onChange={(e) =>
setState((prev) => ({ ...prev, callbackUrl: e.target.value }))
}
placeholder={t(
'Paste the full callback URL (includes code & state)'
)}
autoComplete='off'
spellCheck={false}
/>
<div className='text-muted-foreground text-xs'>
{t(
'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.'
)}
</div>
</div>
</div>
<DialogFooter>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -215,8 +148,68 @@ export function CodexOAuthDialog({ ...@@ -215,8 +148,68 @@ export function CodexOAuthDialog({
)} )}
{state.isCompleting ? t('Generating...') : t('Generate credential')} {state.isCompleting ? t('Generating...') : t('Generate credential')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-4'>
<Alert>
<AlertDescription>
{t(
'1) Click "Open authorization page" and complete login. 2) Your browser may redirect to localhost (it is OK if the page does not load). 3) Copy the full URL from the address bar and paste it below. 4) Click "Generate credential".'
)}
</AlertDescription>
</Alert>
<div className='flex flex-wrap gap-2'>
<Button onClick={handleStart} disabled={state.isStarting}>
{state.isStarting ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : (
<ExternalLink className='mr-2 h-4 w-4' />
)}
{t('Open authorization page')}
</Button>
<Button
type='button'
variant='outline'
disabled={!canCopyAuthorizeUrl}
onClick={async () => {
if (!state.authorizeUrl) return
await copyToClipboard(state.authorizeUrl)
}}
aria-label={t('Copy authorization link')}
title={t('Copy authorization link')}
>
{copiedText === state.authorizeUrl ? (
<Check className='mr-2 h-4 w-4 text-green-600' />
) : (
<Copy className='mr-2 h-4 w-4' />
)}
{t('Copy authorization link')}
</Button>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Callback URL')}</div>
<Input
value={state.callbackUrl}
onChange={(e) =>
setState((prev) => ({ ...prev, callbackUrl: e.target.value }))
}
placeholder={t(
'Paste the full callback URL (includes code & state)'
)}
autoComplete='off'
spellCheck={false}
/>
<div className='text-muted-foreground text-xs'>
{t(
'Tip: The generated key is a JSON credential including access_token / refresh_token / account_id.'
)}
</div>
</div>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react' ...@@ -22,16 +22,9 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { handleCopyChannel } from '../../lib' import { handleCopyChannel } from '../../lib'
import { useChannels } from '../channels-provider' import { useChannels } from '../channels-provider'
...@@ -74,45 +67,20 @@ export function CopyChannelDialog({ ...@@ -74,45 +67,20 @@ export function CopyChannelDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Copy Channel')}</DialogTitle> title={t('Copy Channel')}
<DialogDescription> description={
{t('Create a copy of:')} <strong>{currentRow.name}</strong> <>
</DialogDescription> {t('Create a copy of:')}
</DialogHeader> <strong>{currentRow.name}</strong>
</>
<div className='space-y-4 py-4'> }
<div className='space-y-2'> contentHeight='auto'
<Label htmlFor='suffix'>{t('Name Suffix')}</Label> bodyClassName='space-y-4'
<Input footer={
id='suffix' <>
placeholder={t('_copy')}
value={suffix}
onChange={(e) => setSuffix(e.target.value)}
disabled={isCopying}
/>
<p className='text-muted-foreground text-xs'>
{t('New name will be:')} {currentRow.name}
{suffix}
</p>
</div>
<div className='flex items-center space-x-2'>
<Checkbox
id='reset-balance'
checked={resetBalance}
onCheckedChange={(checked) => setResetBalance(!!checked)}
disabled={isCopying}
/>
<Label htmlFor='reset-balance' className='text-sm font-normal'>
{t('Reset balance and used quota')}
</Label>
</div>
</div>
<DialogFooter>
<Button <Button
variant='outline' variant='outline'
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
...@@ -122,10 +90,39 @@ export function CopyChannelDialog({ ...@@ -122,10 +90,39 @@ export function CopyChannelDialog({
</Button> </Button>
<Button onClick={handleCopy} disabled={isCopying}> <Button onClick={handleCopy} disabled={isCopying}>
{isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} {isCopying && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{isCopying ? 'Copying...' : 'Copy Channel'} {isCopying ? t('Copying...') : t('Copy Channel')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-4 py-4'>
<div className='space-y-2'>
<Label htmlFor='suffix'>{t('Name Suffix')}</Label>
<Input
id='suffix'
placeholder={t('_copy')}
value={suffix}
onChange={(e) => setSuffix(e.target.value)}
disabled={isCopying}
/>
<p className='text-muted-foreground text-xs'>
{t('New name will be:')} {currentRow.name}
{suffix}
</p>
</div>
<div className='flex items-center space-x-2'>
<Checkbox
id='reset-balance'
checked={resetBalance}
onCheckedChange={(checked) => setResetBalance(!!checked)}
disabled={isCopying}
/>
<Label htmlFor='reset-balance' className='text-sm font-normal'>
{t('Reset balance and used quota')}
</Label>
</div>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -21,16 +21,9 @@ import { AlertTriangle } from 'lucide-react' ...@@ -21,16 +21,9 @@ import { AlertTriangle } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
interface StatusCodeRiskDialogProps { interface StatusCodeRiskDialogProps {
open: boolean open: boolean
...@@ -84,73 +77,22 @@ export function StatusCodeRiskDialog({ ...@@ -84,73 +77,22 @@ export function StatusCodeRiskDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle className='text-destructive flex items-center gap-2'> title={
<AlertTriangle className='h-5 w-5' /> <>
{t('High-risk operation confirmation')} <AlertTriangle className='h-5 w-5' />
</DialogTitle> {t('High-risk operation confirmation')}
<DialogDescription> </>
{t('High-risk status code retry risk disclaimer')} }
</DialogDescription> description={t('High-risk status code retry risk disclaimer')}
</DialogHeader> contentClassName='max-w-lg'
titleClassName='text-destructive flex items-center gap-2'
<div className='space-y-4'> contentHeight='auto'
{detailItems.length > 0 && ( bodyClassName='space-y-4'
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'> footer={
<p className='mb-2 text-sm font-medium'> <>
{t('Detected high-risk status code redirect rules')}
</p>
<ul className='list-inside list-disc text-sm'>
{detailItems.map((item) => (
<li key={item} className='font-mono text-xs'>
{item}
</li>
))}
</ul>
</div>
)}
<div className='space-y-2'>
{CHECKLIST_KEYS.map((key, idx) => (
<div key={key} className='flex items-start gap-2'>
<Checkbox
id={`risk-check-${idx}`}
checked={checkedItems.has(idx)}
onCheckedChange={() => toggleCheck(idx)}
/>
<Label
htmlFor={`risk-check-${idx}`}
className='text-sm leading-tight'
>
{t(key)}
</Label>
</div>
))}
</div>
<div className='space-y-1.5'>
<Label className='text-sm'>
{t('Action confirmation')}:{' '}
<code className='bg-muted rounded px-1 text-xs'>
{requiredText}
</code>
</Label>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={t('High-risk status code retry input placeholder')}
/>
{confirmText && !textMatches && (
<p className='text-destructive text-xs'>
{t('High-risk status code retry input mismatch')}
</p>
)}
</div>
</div>
<DialogFooter>
<Button variant='outline' onClick={handleCancel}> <Button variant='outline' onClick={handleCancel}>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
...@@ -161,8 +103,62 @@ export function StatusCodeRiskDialog({ ...@@ -161,8 +103,62 @@ export function StatusCodeRiskDialog({
> >
{t('I confirm enabling high-risk retry')} {t('I confirm enabling high-risk retry')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-4'>
{detailItems.length > 0 && (
<div className='border-destructive/30 bg-destructive/5 rounded-lg border p-3'>
<p className='mb-2 text-sm font-medium'>
{t('Detected high-risk status code redirect rules')}
</p>
<ul className='list-inside list-disc text-sm'>
{detailItems.map((item) => (
<li key={item} className='font-mono text-xs'>
{item}
</li>
))}
</ul>
</div>
)}
<div className='space-y-2'>
{CHECKLIST_KEYS.map((key, idx) => (
<div key={key} className='flex items-start gap-2'>
<Checkbox
id={`risk-check-${idx}`}
checked={checkedItems.has(idx)}
onCheckedChange={() => toggleCheck(idx)}
/>
<Label
htmlFor={`risk-check-${idx}`}
className='text-sm leading-tight'
>
{t(key)}
</Label>
</div>
))}
</div>
<div className='space-y-1.5'>
<Label className='text-sm'>
{t('Action confirmation')}:{' '}
<code className='bg-muted rounded px-1 text-xs'>
{requiredText}
</code>
</Label>
<Input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder={t('High-risk status code retry input placeholder')}
/>
{confirmText && !textMatches && (
<p className='text-destructive text-xs'>
{t('High-risk status code retry input mismatch')}
</p>
)}
</div>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -23,18 +23,11 @@ import { useTranslation } from 'react-i18next' ...@@ -23,18 +23,11 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { MultiSelect } from '@/components/multi-select' import { MultiSelect } from '@/components/multi-select'
import { import {
getTagModels, getTagModels,
...@@ -190,115 +183,118 @@ export function TagBatchEditDialog({ ...@@ -190,115 +183,118 @@ export function TagBatchEditDialog({
if (!currentTag) return null if (!currentTag) return null
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog
<DialogContent className='max-h-[90vh] max-w-2xl overflow-y-auto'> open={open}
<DialogHeader> onOpenChange={handleClose}
<DialogTitle>{t('Batch Edit by Tag')}</DialogTitle> title={t('Batch Edit by Tag')}
<DialogDescription> description={
{t('Edit all channels with tag:')} <strong>{currentTag}</strong> <>
</DialogDescription> {t('Edit all channels with tag:')}
</DialogHeader> <strong>{currentTag}</strong>
</>
{isLoading ? ( }
<div className='flex items-center justify-center py-12'> contentClassName='max-w-2xl'
<Loader2 className='text-muted-foreground h-8 w-8 animate-spin' /> contentHeight='auto'
</div> bodyClassName='space-y-4'
) : ( footer={
!isLoading ? (
<> <>
<div className='space-y-4 py-4'> <Button variant='outline' onClick={handleClose} disabled={isSaving}>
<Alert> {t('Cancel')}
<AlertCircle className='h-4 w-4' /> </Button>
<AlertDescription> <Button onClick={handleSave} disabled={isSaving}>
{t( {isSaving ? (
'All edits are overwrite operations. Leave fields empty to keep current values unchanged.' <Loader2 className='mr-2 h-4 w-4 animate-spin' />
)} ) : null}
</AlertDescription> {isSaving ? t('Saving...') : t('Save Changes')}
</Alert> </Button>
</>
{/* Tag Name */} ) : null
<div className='space-y-2'> }
<Label htmlFor='new-tag'>{t('Tag Name')}</Label> >
<Input {isLoading ? (
id='new-tag' <div className='flex items-center justify-center py-12'>
placeholder={t( <Loader2 className='text-muted-foreground h-8 w-8 animate-spin' />
'Enter new tag name (leave empty to disband tag)' </div>
)} ) : (
value={newTag} <>
onChange={(e) => setNewTag(e.target.value)} <div className='space-y-4 py-4'>
disabled={isSaving} <Alert>
/> <AlertCircle className='h-4 w-4' />
<p className='text-muted-foreground text-xs'> <AlertDescription>
{t('Leave empty to disband the tag')} {t(
</p> 'All edits are overwrite operations. Leave fields empty to keep current values unchanged.'
</div> )}
</AlertDescription>
{/* Models */} </Alert>
<div className='space-y-2'>
<Label htmlFor='models'>{t('Models')}</Label> {/* Tag Name */}
<Textarea <div className='space-y-2'>
id='models' <Label htmlFor='new-tag'>{t('Tag Name')}</Label>
placeholder={t( <Input
'Comma-separated model names (leave empty to keep current)' id='new-tag'
)} placeholder={t(
value={models} 'Enter new tag name (leave empty to disband tag)'
onChange={(e) => setModels(e.target.value)} )}
disabled={isSaving} value={newTag}
rows={3} onChange={(e) => setNewTag(e.target.value)}
/> disabled={isSaving}
<p className='text-muted-foreground text-xs'> />
{t( <p className='text-muted-foreground text-xs'>
'Current models for the longest channel in this tag. May not include all models from all channels.' {t('Leave empty to disband the tag')}
)} </p>
</p> </div>
</div>
{/* Model Mapping */}
<div className='space-y-2'>
<Label htmlFor='model-mapping'>{t('Model Mapping')}</Label>
<ModelMappingEditor
value={modelMapping}
onChange={setModelMapping}
disabled={isSaving}
/>
</div>
{/* Groups */} {/* Models */}
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='groups'>{t('Groups')}</Label> <Label htmlFor='models'>{t('Models')}</Label>
{isLoadingGroups ? ( <Textarea
<Skeleton className='h-10 w-full' /> id='models'
) : ( placeholder={t(
<MultiSelect 'Comma-separated model names (leave empty to keep current)'
options={groupOptions} )}
selected={groups} value={models}
onChange={setGroups} onChange={(e) => setModels(e.target.value)}
placeholder={t( disabled={isSaving}
'Select groups (leave empty to keep current)' rows={3}
)} />
/> <p className='text-muted-foreground text-xs'>
{t(
'Current models for the longest channel in this tag. May not include all models from all channels.'
)} )}
<p className='text-muted-foreground text-xs'> </p>
{t('User groups that can access channels with this tag')}
</p>
</div>
</div> </div>
<DialogFooter> {/* Model Mapping */}
<Button <div className='space-y-2'>
variant='outline' <Label htmlFor='model-mapping'>{t('Model Mapping')}</Label>
onClick={handleClose} <ModelMappingEditor
value={modelMapping}
onChange={setModelMapping}
disabled={isSaving} disabled={isSaving}
> />
{t('Cancel')} </div>
</Button>
<Button onClick={handleSave} disabled={isSaving}> {/* Groups */}
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} <div className='space-y-2'>
{isSaving ? t('Saving...') : t('Save Changes')} <Label htmlFor='groups'>{t('Groups')}</Label>
</Button> {isLoadingGroups ? (
</DialogFooter> <Skeleton className='h-10 w-full' />
</> ) : (
)} <MultiSelect
</DialogContent> options={groupOptions}
selected={groups}
onChange={setGroups}
placeholder={t('Select groups (leave empty to keep current)')}
/>
)}
<p className='text-muted-foreground text-xs'>
{t('User groups that can access channels with this tag')}
</p>
</div>
</div>
</>
)}
</Dialog> </Dialog>
) )
} }
...@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,15 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatDateTimeObject } from '@/lib/time' import { formatDateTimeObject } from '@/lib/time'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Markdown } from '@/components/ui/markdown' import { Markdown } from '@/components/ui/markdown'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
interface AnnouncementDetailModalProps { interface AnnouncementDetailModalProps {
open: boolean open: boolean
...@@ -47,38 +41,39 @@ export function AnnouncementDetailModal({ ...@@ -47,38 +41,39 @@ export function AnnouncementDetailModal({
}: AnnouncementDetailModalProps) { }: AnnouncementDetailModalProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Announcement Details')}</DialogTitle> title={t('Announcement Details')}
{announcement?.publishDate && ( description={
<DialogDescription> announcement?.publishDate
{t('Published:')}{' '} ? `${t('Published:')} ${formatDateTimeObject(new Date(announcement.publishDate))}`
{formatDateTimeObject(new Date(announcement.publishDate))} : undefined
</DialogDescription> }
contentClassName='sm:max-w-lg'
contentHeight='auto'
bodyClassName='space-y-4'
>
<ScrollArea className='max-h-[min(58vh,520px)] pr-4'>
<div className='space-y-4'>
{announcement?.content && (
<div>
<h4 className='mb-2 font-medium'>{t('Content')}</h4>
<Markdown>{announcement.content}</Markdown>
</div>
)} )}
</DialogHeader> {announcement?.extra && (
<ScrollArea className='max-h-[60vh] pr-4'> <div>
<div className='space-y-4'> <h4 className='mb-2 font-medium'>
{announcement?.content && ( {t('Additional Information')}
<div> </h4>
<h4 className='mb-2 font-medium'>{t('Content')}</h4> <Markdown className='text-muted-foreground'>
<Markdown>{announcement.content}</Markdown> {announcement.extra}
</div> </Markdown>
)} </div>
{announcement?.extra && ( )}
<div> </div>
<h4 className='mb-2 font-medium'> </ScrollArea>
{t('Additional Information')}
</h4>
<Markdown className='text-muted-foreground'>
{announcement.extra}
</Markdown>
</div>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -23,15 +23,9 @@ import { toast } from 'sonner' ...@@ -23,15 +23,9 @@ import { toast } from 'sonner'
import { getUserModels } from '@/lib/api' import { getUserModels } from '@/lib/api'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ComboboxInput } from '@/components/ui/combobox-input' import { ComboboxInput } from '@/components/ui/combobox-input'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Dialog } from '@/components/dialog'
const APP_CONFIGS = { const APP_CONFIGS = {
claude: { claude: {
...@@ -151,76 +145,78 @@ export function CCSwitchDialog(props: Props) { ...@@ -151,76 +145,78 @@ export function CCSwitchDialog(props: Props) {
} }
return ( return (
<Dialog open={props.open} onOpenChange={props.onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={props.open}
<DialogHeader> onOpenChange={props.onOpenChange}
<DialogTitle>{t('Import to CC Switch')}</DialogTitle> title={t('Import to CC Switch')}
</DialogHeader> contentClassName='sm:max-w-md'
contentHeight='auto'
<div className='space-y-4'> bodyClassName='space-y-4'
<div className='space-y-2'> footer={
<Label>{t('Application')}</Label> <>
<RadioGroup
value={app}
onValueChange={handleAppChange}
className='flex gap-4'
>
{(
Object.entries(APP_CONFIGS) as [
AppType,
(typeof APP_CONFIGS)[AppType],
][]
).map(([key, cfg]) => (
<div key={key} className='flex items-center gap-2'>
<RadioGroupItem value={key} id={`app-${key}`} />
<Label htmlFor={`app-${key}`} className='cursor-pointer'>
{cfg.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label>{t('Name')}</Label>
<ComboboxInput
options={[]}
value={name}
onValueChange={setName}
placeholder={currentConfig.defaultName}
emptyText=''
allowCustomValue={true}
/>
</div>
{currentConfig.modelFields.map((field) => (
<div key={field.key} className='space-y-2'>
<Label>
{t(field.labelKey)}
{field.required && (
<span className='text-destructive ml-0.5'>*</span>
)}
</Label>
<ComboboxInput
options={modelOptions}
value={models[field.key] || ''}
onValueChange={(v) =>
setModels((prev) => ({ ...prev, [field.key]: v }))
}
placeholder={t('Select or enter model name')}
emptyText={t('No models found')}
/>
</div>
))}
</div>
<DialogFooter>
<Button variant='outline' onClick={() => props.onOpenChange(false)}> <Button variant='outline' onClick={() => props.onOpenChange(false)}>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
<Button onClick={handleSubmit}>{t('Open CC Switch')}</Button> <Button onClick={handleSubmit}>{t('Open CC Switch')}</Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-4'>
<div className='space-y-2'>
<Label>{t('Application')}</Label>
<RadioGroup
value={app}
onValueChange={handleAppChange}
className='flex gap-4'
>
{(
Object.entries(APP_CONFIGS) as [
AppType,
(typeof APP_CONFIGS)[AppType],
][]
).map(([key, cfg]) => (
<div key={key} className='flex items-center gap-2'>
<RadioGroupItem value={key} id={`app-${key}`} />
<Label htmlFor={`app-${key}`} className='cursor-pointer'>
{cfg.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label>{t('Name')}</Label>
<ComboboxInput
options={[]}
value={name}
onValueChange={setName}
placeholder={currentConfig.defaultName}
emptyText=''
allowCustomValue={true}
/>
</div>
{currentConfig.modelFields.map((field) => (
<div key={field.key} className='space-y-2'>
<Label>
{t(field.labelKey)}
{field.required && (
<span className='text-destructive ml-0.5'>*</span>
)}
</Label>
<ComboboxInput
options={modelOptions}
value={models[field.key] || ''}
onValueChange={(v) =>
setModels((prev) => ({ ...prev, [field.key]: v }))
}
placeholder={t('Select or enter model name')}
emptyText={t('No models found')}
/>
</div>
))}
</div>
</Dialog> </Dialog>
) )
} }
...@@ -25,19 +25,12 @@ import { toast } from 'sonner' ...@@ -25,19 +25,12 @@ import { toast } from 'sonner'
import { copyToClipboard } from '@/lib/copy-to-clipboard' import { copyToClipboard } from '@/lib/copy-to-clipboard'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table' import { DataTableBulkActions as BulkActionsToolbar } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import { import {
handleBatchEnableModels, handleBatchEnableModels,
handleBatchDisableModels, handleBatchDisableModels,
...@@ -187,19 +180,17 @@ export function DataTableBulkActions<TData>({ ...@@ -187,19 +180,17 @@ export function DataTableBulkActions<TData>({
</BulkActionsToolbar> </BulkActionsToolbar>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> <Dialog
<DialogContent> open={showDeleteConfirm}
<DialogHeader> onOpenChange={setShowDeleteConfirm}
<DialogTitle>{t('Delete Models?')}</DialogTitle> title={t('Delete Models?')}
<DialogDescription> description={t(
{t( 'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.',
'Are you sure you want to delete {{count}} model(s)? This action cannot be undone.', { count: selectedIds.length }
{ count: selectedIds.length } )}
)} contentHeight='auto'
</DialogDescription> footer={
</DialogHeader> <>
<DialogFooter>
<Button <Button
variant='outline' variant='outline'
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
...@@ -209,8 +200,10 @@ export function DataTableBulkActions<TData>({ ...@@ -209,8 +200,10 @@ export function DataTableBulkActions<TData>({
<Button variant='destructive' onClick={handleDeleteAll}> <Button variant='destructive' onClick={handleDeleteAll}>
{t('Delete')} {t('Delete')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{' '}
</Dialog> </Dialog>
</> </>
) )
......
...@@ -17,14 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,14 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Dialog } from '@/components/dialog'
type DescriptionDialogProps = { type DescriptionDialogProps = {
open: boolean open: boolean
...@@ -41,21 +35,22 @@ export function DescriptionDialog({ ...@@ -41,21 +35,22 @@ export function DescriptionDialog({
}: DescriptionDialogProps) { }: DescriptionDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='max-w-2xl'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{modelName}</DialogTitle> title={modelName}
<DialogDescription>{t('Model Description')}</DialogDescription> description={t('Model Description')}
</DialogHeader> contentClassName='max-w-2xl'
contentHeight='auto'
<ScrollArea className='max-h-96'> bodyClassName='space-y-4'
<div className='space-y-2 pr-4'> >
<p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'> <ScrollArea className='max-h-96'>
{description} <div className='space-y-2 pr-4'>
</p> <p className='text-foreground text-sm leading-relaxed break-words whitespace-pre-wrap'>
</div> {description}
</ScrollArea> </p>
</DialogContent> </div>
</ScrollArea>
</Dialog> </Dialog>
) )
} }
...@@ -22,15 +22,9 @@ import { Loader2 } from 'lucide-react' ...@@ -22,15 +22,9 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Dialog } from '@/components/dialog'
import { estimatePrice, extendDeployment, getDeployment } from '../../api' import { estimatePrice, extendDeployment, getDeployment } from '../../api'
import { deploymentsQueryKeys } from '../../lib' import { deploymentsQueryKeys } from '../../lib'
...@@ -164,62 +158,16 @@ export function ExtendDeploymentDialog({ ...@@ -164,62 +158,16 @@ export function ExtendDeploymentDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Extend deployment')}</DialogTitle> title={t('Extend deployment')}
</DialogHeader> contentClassName='sm:max-w-lg'
footerClassName='mt-4'
{isLoadingDetails ? ( contentHeight='auto'
<div className='flex items-center justify-center py-10'> bodyClassName='space-y-4'
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' /> footer={
</div> <>
) : (
<div className='space-y-4'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Duration (hours)')}</div>
<Input
type='number'
min={1}
value={hours}
onChange={(e) => setHours(toInt(e.target.value, 1))}
/>
<div className='text-muted-foreground text-xs'>
{t('This will extend the deployment by the specified hours.')}
</div>
</div>
<Separator />
<div className='space-y-1'>
<div className='text-sm font-medium'>{t('Estimated cost')}</div>
<div className='text-muted-foreground text-sm'>
{isLoadingPrice || isFetchingPrice ? (
<span className='inline-flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
{t('Calculating...')}
</span>
) : priceParams ? (
priceSummary || t('Not available')
) : (
t('Not available')
)}
</div>
{!priceParams ? (
<div className='text-muted-foreground text-xs'>
{t('Unable to estimate price for this deployment.')}
</div>
) : null}
</div>
</div>
)}
<DialogFooter className='mt-4'>
<Button variant='outline' onClick={() => onOpenChange(false)}> <Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
...@@ -229,8 +177,57 @@ export function ExtendDeploymentDialog({ ...@@ -229,8 +177,57 @@ export function ExtendDeploymentDialog({
) : null} ) : null}
{t('Extend')} {t('Extend')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
{isLoadingDetails ? (
<div className='flex items-center justify-center py-10'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
</div>
) : (
<div className='space-y-4'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<div className='space-y-2'>
<div className='text-sm font-medium'>{t('Duration (hours)')}</div>
<Input
type='number'
min={1}
value={hours}
onChange={(e) => setHours(toInt(e.target.value, 1))}
/>
<div className='text-muted-foreground text-xs'>
{t('This will extend the deployment by the specified hours.')}
</div>
</div>
<Separator />
<div className='space-y-1'>
<div className='text-sm font-medium'>{t('Estimated cost')}</div>
<div className='text-muted-foreground text-sm'>
{isLoadingPrice || isFetchingPrice ? (
<span className='inline-flex items-center gap-2'>
<Loader2 className='h-4 w-4 animate-spin' />
{t('Calculating...')}
</span>
) : priceParams ? (
priceSummary || t('Not available')
) : (
t('Not available')
)}
</div>
{!priceParams ? (
<div className='text-muted-foreground text-xs'>
{t('Unable to estimate price for this deployment.')}
</div>
) : null}
</div>
</div>
)}
</Dialog> </Dialog>
) )
} }
...@@ -22,14 +22,8 @@ import { Loader2 } from 'lucide-react' ...@@ -22,14 +22,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
import { checkClusterNameAvailability, updateDeploymentName } from '../../api' import { checkClusterNameAvailability, updateDeploymentName } from '../../api'
import { deploymentsQueryKeys } from '../../lib' import { deploymentsQueryKeys } from '../../lib'
...@@ -111,27 +105,16 @@ export function RenameDeploymentDialog({ ...@@ -111,27 +105,16 @@ export function RenameDeploymentDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-lg'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Rename deployment')}</DialogTitle> title={t('Rename deployment')}
</DialogHeader> contentClassName='sm:max-w-lg'
footerClassName='mt-4'
<div className='space-y-2'> contentHeight='auto'
<div className='text-muted-foreground text-sm'> bodyClassName='space-y-4'
{t('Deployment ID')}:{' '} footer={
<span className='font-mono'>{deploymentId}</span> <>
</div>
<Input
placeholder={t('Enter a new name')}
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete='off'
/>
<div className='text-muted-foreground text-xs'>{helper}</div>
</div>
<DialogFooter className='mt-4'>
<Button variant='outline' onClick={() => onOpenChange(false)}> <Button variant='outline' onClick={() => onOpenChange(false)}>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
...@@ -141,8 +124,22 @@ export function RenameDeploymentDialog({ ...@@ -141,8 +124,22 @@ export function RenameDeploymentDialog({
) : null} ) : null}
{t('Rename')} {t('Rename')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-2'>
<div className='text-muted-foreground text-sm'>
{t('Deployment ID')}:{' '}
<span className='font-mono'>{deploymentId}</span>
</div>
<Input
placeholder={t('Enter a new name')}
value={name}
onChange={(e) => setName(e.target.value)}
autoComplete='off'
/>
<div className='text-muted-foreground text-xs'>{helper}</div>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -24,16 +24,9 @@ import { toast } from 'sonner' ...@@ -24,16 +24,9 @@ import { toast } from 'sonner'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useIsMobile } from '@/hooks/use-mobile' import { useIsMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { syncUpstream, previewUpstreamDiff } from '../../api' import { syncUpstream, previewUpstreamDiff } from '../../api'
import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants' import { getSyncLocaleOptions, getSyncSourceOptions } from '../../constants'
...@@ -125,117 +118,16 @@ export function SyncWizardDialog({ ...@@ -125,117 +118,16 @@ export function SyncWizardDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent open={open}
className='flex max-h-[90vh] w-full flex-col gap-4 p-4 sm:max-w-2xl sm:p-6' onOpenChange={onOpenChange}
initialFocus={!isMobile} title={t('Sync Upstream Models')}
> description={t('Synchronize models and vendors from an upstream source')}
<DialogHeader className='flex-shrink-0 text-start'> initialFocus={!isMobile}
<DialogTitle>{t('Sync Upstream Models')}</DialogTitle> contentHeight='auto'
<DialogDescription> bodyClassName='flex flex-col gap-6'
{t('Synchronize models and vendors from an upstream source')} footer={
</DialogDescription> <>
</DialogHeader>
<div className='flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto'>
<div className='space-y-3'>
<div>
<Label className='text-base'>{t('Select Sync Source')}</Label>
<p className='text-muted-foreground text-sm'>
{t('Choose where to fetch upstream metadata.')}
</p>
</div>
<RadioGroup
value={source}
onValueChange={(value) => {
const selected = SYNC_SOURCE_OPTIONS.find(
(option) => option.value === value
)
if (!selected || selected.disabled) return
setSource(selected.value)
}}
className='grid gap-3 md:grid-cols-2'
>
{SYNC_SOURCE_OPTIONS.map((option) => {
const isActive = source === option.value
const isDisabled = option.disabled
return (
<Label
key={option.value}
htmlFor={`sync-source-${option.value}`}
className={cn(
'flex-col items-start gap-0 rounded-lg border p-4 font-normal transition-all',
isActive && 'border-primary ring-primary ring-1',
isDisabled
? 'cursor-not-allowed opacity-60'
: 'hover:border-primary/60 cursor-pointer'
)}
>
<div className='flex items-start gap-3'>
<RadioGroupItem
value={option.value}
id={`sync-source-${option.value}`}
disabled={isDisabled}
/>
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<span className='font-medium'>{option.label}</span>
{option.value === 'official' && (
<StatusBadge
label='Default'
variant='neutral'
copyable={false}
/>
)}
</div>
<p className='text-muted-foreground text-sm'>
{option.description}
</p>
</div>
</div>
</Label>
)
})}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label className='text-base'>{t('Select Language')}</Label>
<RadioGroup
value={locale}
onValueChange={(v) => setLocale(v as SyncLocale)}
className='grid gap-3 sm:grid-cols-3'
>
{SYNC_LOCALE_OPTIONS.map((option) => (
<div
key={option.value}
className='flex items-center space-x-2 rounded-lg border p-3'
>
<RadioGroupItem
value={option.value}
id={`locale-${option.value}`}
/>
<Label
htmlFor={`locale-${option.value}`}
className='cursor-pointer font-normal'
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='bg-muted/50 rounded-lg border p-4'>
<p className='text-muted-foreground text-sm'>
{t(
'The sync will fetch missing models and vendors from the selected source. Existing records are updated only when you approve conflicts.'
)}
</p>
</div>
</div>
<DialogFooter className='flex-shrink-0 gap-2 sm:justify-end'>
<Button <Button
variant='outline' variant='outline'
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
...@@ -246,10 +138,106 @@ export function SyncWizardDialog({ ...@@ -246,10 +138,106 @@ export function SyncWizardDialog({
<Button onClick={handleSync} disabled={isSyncing}> <Button onClick={handleSync} disabled={isSyncing}>
{isSyncing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} {isSyncing && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
<RefreshCw className='mr-2 h-4 w-4' /> <RefreshCw className='mr-2 h-4 w-4' />
{isSyncing ? 'Syncing...' : 'Sync Now'} {isSyncing ? t('Syncing...') : t('Sync Now')}
</Button> </Button>
</DialogFooter> </>
</DialogContent> }
>
<div className='space-y-3'>
<div>
<Label className='text-base'>{t('Select Sync Source')}</Label>
<p className='text-muted-foreground text-sm'>
{t('Choose where to fetch upstream metadata.')}
</p>
</div>
<RadioGroup
value={source}
onValueChange={(value) => {
const selected = SYNC_SOURCE_OPTIONS.find(
(option) => option.value === value
)
if (!selected || selected.disabled) return
setSource(selected.value)
}}
className='grid gap-3 md:grid-cols-2'
>
{SYNC_SOURCE_OPTIONS.map((option) => {
const isActive = source === option.value
const isDisabled = option.disabled
return (
<Label
key={option.value}
htmlFor={`sync-source-${option.value}`}
className={cn(
'flex-col items-start gap-0 rounded-lg border p-4 font-normal transition-all',
isActive && 'border-primary ring-primary ring-1',
isDisabled
? 'cursor-not-allowed opacity-60'
: 'hover:border-primary/60 cursor-pointer'
)}
>
<div className='flex items-start gap-3'>
<RadioGroupItem
value={option.value}
id={`sync-source-${option.value}`}
disabled={isDisabled}
/>
<div className='space-y-1'>
<div className='flex items-center gap-2'>
<span className='font-medium'>{option.label}</span>
{option.value === 'official' && (
<StatusBadge
label='Default'
variant='neutral'
copyable={false}
/>
)}
</div>
<p className='text-muted-foreground text-sm'>
{option.description}
</p>
</div>
</div>
</Label>
)
})}
</RadioGroup>
</div>
<div className='space-y-2'>
<Label className='text-base'>{t('Select Language')}</Label>
<RadioGroup
value={locale}
onValueChange={(v) => setLocale(v as SyncLocale)}
className='grid gap-3 sm:grid-cols-3'
>
{SYNC_LOCALE_OPTIONS.map((option) => (
<div
key={option.value}
className='flex items-center space-x-2 rounded-lg border p-3'
>
<RadioGroupItem
value={option.value}
id={`locale-${option.value}`}
/>
<Label
htmlFor={`locale-${option.value}`}
className='cursor-pointer font-normal'
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='bg-muted/50 rounded-lg border p-4'>
<p className='text-muted-foreground text-sm'>
{t(
'The sync will fetch missing models and vendors from the selected source. Existing records are updated only when you approve conflicts.'
)}
</p>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -25,14 +25,6 @@ import { useTranslation } from 'react-i18next' ...@@ -25,14 +25,6 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -43,6 +35,7 @@ import { ...@@ -43,6 +35,7 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { createVendor, updateVendor } from '../../api' import { createVendor, updateVendor } from '../../api'
import { vendorsQueryKeys, modelsQueryKeys } from '../../lib' import { vendorsQueryKeys, modelsQueryKeys } from '../../lib'
import { vendorFormSchema, type Vendor } from '../../types' import { vendorFormSchema, type Vendor } from '../../types'
...@@ -53,6 +46,8 @@ type VendorMutateDialogProps = { ...@@ -53,6 +46,8 @@ type VendorMutateDialogProps = {
currentVendor?: Vendor | null currentVendor?: Vendor | null
} }
const VENDOR_MUTATE_FORM_ID = 'vendor-mutate-form'
export function VendorMutateDialog({ export function VendorMutateDialog({
open, open,
onOpenChange, onOpenChange,
...@@ -118,98 +113,107 @@ export function VendorMutateDialog({ ...@@ -118,98 +113,107 @@ export function VendorMutateDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle> title={isEdit ? t('Edit Vendor') : t('Create Vendor')}
{isEdit ? t('Edit Vendor') : t('Create Vendor')} description={
</DialogTitle> isEdit
<DialogDescription> ? t('Update vendor information for {{name}}', {
{isEdit name: currentVendor?.name,
? t('Update vendor information for {{name}}', { })
name: currentVendor?.name, : t('Add a new vendor to the system')
}) }
: t('Add a new vendor to the system')} contentHeight='auto'
</DialogDescription> bodyClassName='space-y-4'
</DialogHeader> footer={
<>
<Form {...form}> <Button
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'> type='button'
<FormField variant='outline'
control={form.control} onClick={() => onOpenChange(false)}
name='name' disabled={isSaving}
render={({ field }) => ( >
<FormItem> {t('Cancel')}
<FormLabel>{t('Vendor Name *')}</FormLabel> </Button>
<FormControl> <Button
<Input type='submit'
placeholder={t('OpenAI, Anthropic, etc.')} form={VENDOR_MUTATE_FORM_ID}
{...field} disabled={isSaving}
/> >
</FormControl> {isSaving ? (
<FormDescription> <Loader2 className='mr-2 h-4 w-4 animate-spin' />
{t('The unique name for this vendor')} ) : null}
</FormDescription> {isSaving ? t('Saving...') : isEdit ? t('Update') : t('Create')}
<FormMessage /> </Button>
</FormItem> </>
)} }
/> >
<Form {...form}>
<FormField <form
control={form.control} id={VENDOR_MUTATE_FORM_ID}
name='description' onSubmit={form.handleSubmit(onSubmit)}
render={({ field }) => ( className='space-y-4'
<FormItem> >
<FormLabel>{t('Description')}</FormLabel> <FormField
<FormControl> control={form.control}
<Textarea name='name'
placeholder={t('Describe this vendor...')} render={({ field }) => (
rows={3} <FormItem>
{...field} <FormLabel>{t('Vendor Name *')}</FormLabel>
/> <FormControl>
</FormControl> <Input
<FormMessage /> placeholder={t('OpenAI, Anthropic, etc.')}
</FormItem> {...field}
)} />
/> </FormControl>
<FormDescription>
{t('The unique name for this vendor')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name='icon' name='description'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('Icon')}</FormLabel> <FormLabel>{t('Description')}</FormLabel>
<FormControl> <FormControl>
<Input <Textarea
placeholder={t('OpenAI, Anthropic, Google, etc.')} placeholder={t('Describe this vendor...')}
{...field} rows={3}
/> {...field}
</FormControl> />
<FormDescription> </FormControl>
{t('@lobehub/icons key name')} <FormMessage />
</FormDescription> </FormItem>
<FormMessage /> )}
</FormItem> />
)}
/>
<DialogFooter> <FormField
<Button control={form.control}
type='button' name='icon'
variant='outline' render={({ field }) => (
onClick={() => onOpenChange(false)} <FormItem>
disabled={isSaving} <FormLabel>{t('Icon')}</FormLabel>
> <FormControl>
{t('Cancel')} <Input
</Button> placeholder={t('OpenAI, Anthropic, Google, etc.')}
<Button type='submit' disabled={isSaving}> {...field}
{isSaving && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} />
{isSaving ? 'Saving...' : isEdit ? 'Update' : 'Create'} </FormControl>
</Button> <FormDescription>
</DialogFooter> {t('@lobehub/icons key name')}
</form> </FormDescription>
</Form> <FormMessage />
</DialogContent> </FormItem>
)}
/>
</form>
</Form>
</Dialog> </Dialog>
) )
} }
...@@ -32,12 +32,6 @@ import { formatQuotaWithCurrency } from '@/lib/currency' ...@@ -32,12 +32,6 @@ import { formatQuotaWithCurrency } from '@/lib/currency'
import dayjs from '@/lib/dayjs' import dayjs from '@/lib/dayjs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { import {
Tooltip, Tooltip,
...@@ -45,6 +39,7 @@ import { ...@@ -45,6 +39,7 @@ import {
TooltipTrigger, TooltipTrigger,
TooltipProvider, TooltipProvider,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { Dialog } from '@/components/dialog'
import { Turnstile } from '@/components/turnstile' import { Turnstile } from '@/components/turnstile'
import { getCheckinStatus, performCheckin } from '../api' import { getCheckinStatus, performCheckin } from '../api'
import type { CheckinRecord } from '../types' import type { CheckinRecord } from '../types'
...@@ -253,27 +248,26 @@ export function CheckinCalendarCard({ ...@@ -253,27 +248,26 @@ export function CheckinCalendarCard({
setTurnstileWidgetKey((v) => v + 1) setTurnstileWidgetKey((v) => v + 1)
} }
}} }}
title={t('Security Check')}
contentClassName='sm:max-w-md'
contentHeight='auto'
bodyClassName='space-y-4'
> >
<DialogContent className='sm:max-w-md'> <div className='text-muted-foreground text-sm'>
<DialogHeader> {t('Please complete the security check to continue.')}
<DialogTitle>{t('Security Check')}</DialogTitle> </div>
</DialogHeader> <div className='flex justify-center py-4'>
<div className='text-muted-foreground text-sm'> <Turnstile
{t('Please complete the security check to continue.')} key={turnstileWidgetKey}
</div> siteKey={turnstileSiteKey}
<div className='flex justify-center py-4'> onVerify={(token) => {
<Turnstile doCheckin(token)
key={turnstileWidgetKey} }}
siteKey={turnstileSiteKey} onExpire={() => {
onVerify={(token) => { setTurnstileWidgetKey((v) => v + 1)
doCheckin(token) }}
}} />
onExpire={() => { </div>
setTurnstileWidgetKey((v) => v + 1)
}}
/>
</div>
</DialogContent>
</Dialog> </Dialog>
<div className='bg-card overflow-hidden rounded-2xl border'> <div className='bg-card overflow-hidden rounded-2xl border'>
......
...@@ -20,17 +20,10 @@ import { useEffect } from 'react' ...@@ -20,17 +20,10 @@ import { useEffect } from 'react'
import { RefreshCw, Loader2 } from 'lucide-react' import { RefreshCw, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button' import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
import { useAccessToken } from '../../hooks' import { useAccessToken } from '../../hooks'
// ============================================================================ // ============================================================================
...@@ -57,45 +50,18 @@ export function AccessTokenDialog({ ...@@ -57,45 +50,18 @@ export function AccessTokenDialog({
}, [open, token, generate]) }, [open, token, generate])
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Access Token')}</DialogTitle> title={t('Access Token')}
<DialogDescription> description={t(
{t( "Your system access token for API authentication. Keep it secure and don't share it with others."
"Your system access token for API authentication. Keep it secure and don't share it with others." )}
)} contentClassName='sm:max-w-md'
</DialogDescription> contentHeight='auto'
</DialogHeader> bodyClassName='space-y-4'
footer={
<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>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -116,8 +82,35 @@ export function AccessTokenDialog({ ...@@ -116,8 +82,35 @@ export function AccessTokenDialog({
)} )}
{generating ? t('Generating...') : t('Regenerate')} {generating ? t('Generating...') : t('Regenerate')}
</Button> </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> </Dialog>
) )
} }
...@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react' ...@@ -21,15 +21,8 @@ import { Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { PasswordInput } from '@/components/password-input' import { PasswordInput } from '@/components/password-input'
import { updateUserProfile } from '../../api' import { updateUserProfile } from '../../api'
...@@ -114,82 +107,79 @@ export function ChangePasswordDialog({ ...@@ -114,82 +107,79 @@ export function ChangePasswordDialog({
} }
} }
const formId = 'change-password-form'
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<form onSubmit={handleSubmit}> onOpenChange={onOpenChange}
<DialogHeader> title={t('Change Password')}
<DialogTitle>{t('Change Password')}</DialogTitle> description={
<DialogDescription> <>
{t('Update your password for account:')}{' '} {t('Update your password for account:')} <strong>{username}</strong>
<strong>{username}</strong> </>
</DialogDescription> }
</DialogHeader> contentClassName='sm:max-w-md'
contentHeight='auto'
<div className='my-6 space-y-4'> bodyClassName='space-y-4'
<div className='space-y-2'> footer={
<Label htmlFor='currentPassword'>{t('Current Password')}</Label> <>
<PasswordInput <Button
id='currentPassword' type='button'
value={formData.originalPassword} variant='outline'
onChange={(e) => onClick={() => onOpenChange(false)}
handleChange('originalPassword', e.target.value) disabled={loading}
} >
disabled={loading} {t('Cancel')}
required </Button>
autoComplete='current-password' <Button type='submit' form={formId} disabled={loading}>
/> {loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
</div> {loading ? t('Changing...') : t('Change Password')}
</Button>
<div className='space-y-2'> </>
<Label htmlFor='newPassword'>{t('New Password')}</Label> }
<PasswordInput >
id='newPassword' <form id={formId} onSubmit={handleSubmit} className='space-y-4'>
value={formData.newPassword} <div className='space-y-2'>
onChange={(e) => handleChange('newPassword', e.target.value)} <Label htmlFor='currentPassword'>{t('Current Password')}</Label>
disabled={loading} <PasswordInput
required id='currentPassword'
minLength={8} value={formData.originalPassword}
autoComplete='new-password' onChange={(e) => handleChange('originalPassword', e.target.value)}
/> disabled={loading}
<p className='text-muted-foreground text-xs'> required
{t('Must be at least 8 characters')} autoComplete='current-password'
</p> />
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<Label htmlFor='confirmPassword'> <Label htmlFor='newPassword'>{t('New Password')}</Label>
{t('Confirm New Password')} <PasswordInput
</Label> id='newPassword'
<PasswordInput value={formData.newPassword}
id='confirmPassword' onChange={(e) => handleChange('newPassword', e.target.value)}
value={formData.confirmPassword} disabled={loading}
onChange={(e) => required
handleChange('confirmPassword', e.target.value) minLength={8}
} autoComplete='new-password'
disabled={loading} />
required <p className='text-muted-foreground text-xs'>
autoComplete='new-password' {t('Must be at least 8 characters')}
/> </p>
</div> </div>
</div>
<div className='space-y-2'>
<DialogFooter> <Label htmlFor='confirmPassword'>{t('Confirm New Password')}</Label>
<Button <PasswordInput
type='button' id='confirmPassword'
variant='outline' value={formData.confirmPassword}
onClick={() => onOpenChange(false)} onChange={(e) => handleChange('confirmPassword', e.target.value)}
disabled={loading} disabled={loading}
> required
{t('Cancel')} autoComplete='new-password'
</Button> />
<Button type='submit' disabled={loading}> </div>
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} </form>
{loading ? t('Changing...') : t('Change Password')}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog> </Dialog>
) )
} }
...@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store' ...@@ -25,16 +25,9 @@ import { useAuthStore } from '@/stores/auth-store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { deleteUserAccount } from '../../api' import { deleteUserAccount } from '../../api'
// ============================================================================ // ============================================================================
...@@ -101,45 +94,24 @@ export function DeleteAccountDialog({ ...@@ -101,45 +94,24 @@ export function DeleteAccountDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle className='text-destructive flex items-center gap-2'> title={
<AlertTriangle className='h-5 w-5' /> <>
{t('Delete Account')} <AlertTriangle className='h-5 w-5' />
</DialogTitle> {t('Delete Account')}
<DialogDescription> </>
{t( }
'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.' description={t(
)} 'This action cannot be undone. This will permanently delete your account and remove all your data from our servers.'
</DialogDescription> )}
</DialogHeader> contentClassName='sm:max-w-md'
titleClassName='text-destructive flex items-center gap-2'
<div className='my-6 space-y-4'> contentHeight='auto'
<Alert variant='destructive'> bodyClassName='space-y-4'
<AlertTriangle className='h-4 w-4' /> footer={
<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>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -157,8 +129,32 @@ export function DeleteAccountDialog({ ...@@ -157,8 +129,32 @@ export function DeleteAccountDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} {loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Deleting...') : t('Delete Account')} {loading ? t('Deleting...') : t('Delete Account')}
</Button> </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> </Dialog>
) )
} }
...@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next' ...@@ -22,16 +22,9 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useCountdown } from '@/hooks/use-countdown' import { useCountdown } from '@/hooks/use-countdown'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
import { sendEmailVerification, bindEmail } from '../../api' import { sendEmailVerification, bindEmail } from '../../api'
// ============================================================================ // ============================================================================
...@@ -129,60 +122,22 @@ export function EmailBindDialog({ ...@@ -129,60 +122,22 @@ export function EmailBindDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle>{t('Bind Email')}</DialogTitle> title={t('Bind Email')}
<DialogDescription> description={
{currentEmail currentEmail
? t('Current email: {{email}}. Enter a new email to change.', { ? t('Current email: {{email}}. Enter a new email to change.', {
email: currentEmail, email: currentEmail,
}) })
: t('Bind an email address to your account.')} : t('Bind an email address to your account.')
</DialogDescription> }
</DialogHeader> contentClassName='sm:max-w-md'
contentHeight='auto'
<div className='space-y-4 py-4'> bodyClassName='space-y-4'
<div className='space-y-2'> footer={
<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>
<Button <Button
type='button' type='button'
variant='outline' variant='outline'
...@@ -199,8 +154,48 @@ export function EmailBindDialog({ ...@@ -199,8 +154,48 @@ export function EmailBindDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} {loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Binding...') : t('Bind Email')} {loading ? t('Binding...') : t('Bind Email')}
</Button> </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> </Dialog>
) )
} }
...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { Send } from 'lucide-react' import { Send } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { import { Dialog } from '@/components/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
// ============================================================================ // ============================================================================
// Telegram Bind Dialog Component // Telegram Bind Dialog Component
...@@ -45,56 +39,55 @@ export function TelegramBindDialog({ ...@@ -45,56 +39,55 @@ export function TelegramBindDialog({
}: TelegramBindDialogProps) { }: TelegramBindDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Bind Telegram Account')}</DialogTitle> title={t('Bind Telegram Account')}
<DialogDescription> description={t('Click the button below to bind your Telegram account')}
{t('Click the button below to bind your Telegram account')} contentClassName='sm:max-w-md'
</DialogDescription> contentHeight='auto'
</DialogHeader> 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'> <div className='flex flex-col items-center justify-center gap-4 rounded-lg border p-6'>
<Alert> <div className='flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 dark:bg-blue-900'>
<Send className='h-4 w-4' /> <Send className='h-6 w-6 text-blue-600 dark:text-blue-400' />
<AlertDescription> </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( {t(
'You will be redirected to Telegram to complete the binding process.' "After clicking the button, you'll be asked to authorize the bot"
)} )}
</AlertDescription> </p>
</Alert> </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>
<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 */} {/* Telegram Login Widget will be injected here by react-telegram-login */}
<div id='telegram-login-widget' className='flex justify-center'> <div id='telegram-login-widget' className='flex justify-center'>
{/* This would require the react-telegram-login library */} {/* This would require the react-telegram-login library */}
<div className='text-muted-foreground rounded-lg border border-dashed px-6 py-3 text-sm'> <div className='text-muted-foreground rounded-lg border border-dashed px-6 py-3 text-sm'>
{t('Telegram Login Widget')} {t('Telegram Login Widget')}
</div>
</div> </div>
</div> </div>
<p className='text-muted-foreground text-center text-xs'>
{t('The binding will complete automatically after authorization')}
</p>
</div> </div>
</DialogContent>
<p className='text-muted-foreground text-center text-xs'>
{t('The binding will complete automatically after authorization')}
</p>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -23,17 +23,10 @@ import { toast } from 'sonner' ...@@ -23,17 +23,10 @@ import { toast } from 'sonner'
import { regenerate2FABackupCodes } from '@/lib/api' import { regenerate2FABackupCodes } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { CopyButton } from '@/components/copy-button' import { CopyButton } from '@/components/copy-button'
import { Dialog } from '@/components/dialog'
// ============================================================================ // ============================================================================
// Two-FA Backup Codes Dialog Component // Two-FA Backup Codes Dialog Component
...@@ -94,82 +87,26 @@ export function TwoFABackupDialog({ ...@@ -94,82 +87,26 @@ export function TwoFABackupDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle className='flex items-center gap-2'> title={
<RefreshCw className='h-5 w-5' /> <>
{t('Regenerate Backup Codes')} <RefreshCw className='h-5 w-5' />
</DialogTitle> {t('Regenerate Backup Codes')}
<DialogDescription> </>
{backupCodes.length > 0 }
? t('Your new backup codes are ready') description={
: t('Generate new backup codes for account recovery')} backupCodes.length > 0
</DialogDescription> ? t('Your new backup codes are ready')
</DialogHeader> : t('Generate new backup codes for account recovery')
}
<div className='space-y-4 py-4'> contentClassName='sm:max-w-md'
{backupCodes.length === 0 ? ( titleClassName='flex items-center gap-2'
<> contentHeight='auto'
<Alert> bodyClassName='space-y-4'
<AlertDescription> footer={
{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>
{backupCodes.length === 0 ? ( {backupCodes.length === 0 ? (
<> <>
<Button <Button
...@@ -187,8 +124,69 @@ export function TwoFABackupDialog({ ...@@ -187,8 +124,69 @@ export function TwoFABackupDialog({
) : ( ) : (
<Button onClick={handleDone}>{t('Done')}</Button> <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> </Dialog>
) )
} }
...@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api' ...@@ -24,16 +24,9 @@ import { disable2FA } from '@/lib/api'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Dialog } from '@/components/dialog'
// ============================================================================ // ============================================================================
// Two-FA Disable Dialog Component // Two-FA Disable Dialog Component
...@@ -98,60 +91,24 @@ export function TwoFADisableDialog({ ...@@ -98,60 +91,24 @@ export function TwoFADisableDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={handleOpenChange}
<DialogTitle className='text-destructive flex items-center gap-2'> title={
<AlertTriangle className='h-5 w-5' /> <>
{t('Disable Two-Factor Authentication')} <AlertTriangle className='h-5 w-5' />
</DialogTitle> {t('Disable Two-Factor Authentication')}
<DialogDescription> </>
{t( }
'This action will permanently remove 2FA protection from your account.' description={t(
)} 'This action will permanently remove 2FA protection from your account.'
</DialogDescription> )}
</DialogHeader> contentClassName='sm:max-w-md'
titleClassName='text-destructive flex items-center gap-2'
<div className='space-y-4 py-4'> contentHeight='auto'
<Alert variant='destructive'> bodyClassName='space-y-4'
<AlertTriangle className='h-4 w-4' /> footer={
<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>
<Button <Button
variant='outline' variant='outline'
onClick={() => handleOpenChange(false)} onClick={() => handleOpenChange(false)}
...@@ -167,8 +124,47 @@ export function TwoFADisableDialog({ ...@@ -167,8 +124,47 @@ export function TwoFADisableDialog({
{loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />} {loading && <Loader2 className='mr-2 h-4 w-4 animate-spin' />}
{loading ? t('Disabling...') : t('Disable 2FA')} {loading ? t('Disabling...') : t('Disable 2FA')}
</Button> </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> </Dialog>
) )
} }
...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,13 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { QrCode } from 'lucide-react' import { QrCode } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { import { Dialog } from '@/components/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
// ============================================================================ // ============================================================================
// WeChat Bind Dialog Component // WeChat Bind Dialog Component
...@@ -43,40 +37,39 @@ export function WeChatBindDialog({ ...@@ -43,40 +37,39 @@ export function WeChatBindDialog({
}: WeChatBindDialogProps) { }: WeChatBindDialogProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-md'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle>{t('Bind WeChat Account')}</DialogTitle> title={t('Bind WeChat Account')}
<DialogDescription> description={t('Scan the QR code with WeChat to bind your account')}
{t('Scan the QR code with WeChat to bind your account')} contentClassName='sm:max-w-md'
</DialogDescription> contentHeight='auto'
</DialogHeader> 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'> <div className='flex flex-col items-center justify-center rounded-lg border border-dashed p-8'>
<Alert> <QrCode className='text-muted-foreground mb-3 h-16 w-16' />
<QrCode className='h-4 w-4' /> <p className='text-muted-foreground text-sm'>
<AlertDescription> {t('WeChat QR code will be displayed here')}
{t( </p>
'Please use WeChat\'s "Scan QR Code" feature to complete the binding process.' <p className='text-muted-foreground mt-2 text-xs'>
)} {t('This feature requires server-side WeChat configuration')}
</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')}
</p> </p>
</div> </div>
</DialogContent>
<p className='text-muted-foreground text-center text-xs'>
{t('After scanning, the binding will complete automatically')}
</p>
</div>
</Dialog> </Dialog>
) )
} }
...@@ -23,14 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod' ...@@ -23,14 +23,6 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -40,6 +32,7 @@ import { ...@@ -40,6 +32,7 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Dialog } from '@/components/dialog'
const createChatDialogSchema = (t: (key: string) => string) => const createChatDialogSchema = (t: (key: string) => string) =>
z.object({ z.object({
...@@ -49,6 +42,8 @@ const createChatDialogSchema = (t: (key: string) => string) => ...@@ -49,6 +42,8 @@ const createChatDialogSchema = (t: (key: string) => string) =>
type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>> type ChatDialogFormValues = z.infer<ReturnType<typeof createChatDialogSchema>>
const CHAT_DIALOG_FORM_ID = 'chat-dialog-form'
export type ChatEntryData = { export type ChatEntryData = {
name: string name: string
url: string url: string
...@@ -97,74 +92,73 @@ export function ChatDialog({ ...@@ -97,74 +92,73 @@ export function ChatDialog({
} }
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog
<DialogContent className='sm:max-w-[500px]'> open={open}
<DialogHeader> onOpenChange={onOpenChange}
<DialogTitle> title={isEditMode ? t('Edit chat preset') : t('Add chat preset')}
{isEditMode ? t('Edit chat preset') : t('Add chat preset')} description={t('Configure a predefined chat link for end users.')}
</DialogTitle> contentClassName='sm:max-w-[500px]'
<DialogDescription> contentHeight='auto'
{t('Configure a predefined chat link for end users.')} bodyClassName='space-y-4'
</DialogDescription> footer={
</DialogHeader> <>
<Button
<Form {...form}> type='button'
<form variant='outline'
onSubmit={form.handleSubmit(handleSubmit)} onClick={() => onOpenChange(false)}
className='space-y-4'
> >
<FormField {t('Cancel')}
control={form.control} </Button>
name='name' <Button type='submit' form={CHAT_DIALOG_FORM_ID}>
render={({ field }) => ( {isEditMode ? t('Update') : t('Add')}
<FormItem> </Button>
<FormLabel>{t('Chat Client Name')}</FormLabel> </>
<FormControl> }
<Input >
placeholder={t('Please enter chat client name')} <Form {...form}>
{...field} <form
/> id={CHAT_DIALOG_FORM_ID}
</FormControl> onSubmit={form.handleSubmit(handleSubmit)}
<FormDescription> className='space-y-4'
{t('Display name for this chat client.')} >
</FormDescription> <FormField
<FormMessage /> control={form.control}
</FormItem> name='name'
)} render={({ field }) => (
/> <FormItem>
<FormLabel>{t('Chat Client Name')}</FormLabel>
<FormField <FormControl>
control={form.control} <Input
name='url' placeholder={t('Please enter chat client name')}
render={({ field }) => ( {...field}
<FormItem> />
<FormLabel>{t('URL')}</FormLabel> </FormControl>
<FormControl> <FormDescription>
<Input placeholder={t('Please enter the URL')} {...field} /> {t('Display name for this chat client.')}
</FormControl> </FormDescription>
<FormDescription> <FormMessage />
{t('The URL for this chat client.')} </FormItem>
</FormDescription> )}
<FormMessage /> />
</FormItem>
)} <FormField
/> control={form.control}
name='url'
<DialogFooter> render={({ field }) => (
<Button <FormItem>
type='button' <FormLabel>{t('URL')}</FormLabel>
variant='outline' <FormControl>
onClick={() => onOpenChange(false)} <Input placeholder={t('Please enter the URL')} {...field} />
> </FormControl>
{t('Cancel')} <FormDescription>
</Button> {t('The URL for this chat client.')}
<Button type='submit'> </FormDescription>
{isEditMode ? t('Update') : t('Add')} <FormMessage />
</Button> </FormItem>
</DialogFooter> )}
</form> />
</Form> </form>
</DialogContent> </Form>
</Dialog> </Dialog>
) )
} }
...@@ -36,14 +36,6 @@ import { ...@@ -36,14 +36,6 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -62,6 +54,7 @@ import { ...@@ -62,6 +54,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -90,6 +83,8 @@ const faqSchema = z.object({ ...@@ -90,6 +83,8 @@ const faqSchema = z.object({
type FAQFormValues = z.infer<typeof faqSchema> type FAQFormValues = z.infer<typeof faqSchema>
const FAQ_FORM_ID = 'faq-form'
export function FAQSection({ enabled, data }: FAQSectionProps) { export function FAQSection({ enabled, data }: FAQSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const updateOption = useUpdateOption() const updateOption = useUpdateOption()
...@@ -348,79 +343,78 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { ...@@ -348,79 +343,78 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
</div> </div>
</div> </div>
<Dialog open={showDialog} onOpenChange={setShowDialog}> <Dialog
<DialogContent className='max-w-2xl'> open={showDialog}
<DialogHeader> onOpenChange={setShowDialog}
<DialogTitle> title={editingFaq ? t('Edit FAQ') : t('Add FAQ')}
{editingFaq ? t('Edit FAQ') : t('Add FAQ')} description={t('Create or update frequently asked questions for users')}
</DialogTitle> contentClassName='max-w-2xl'
<DialogDescription> contentHeight='auto'
{t('Create or update frequently asked questions for users')} bodyClassName='space-y-4'
</DialogDescription> footer={
</DialogHeader> <>
<Form {...form}> <Button
<form type='button'
onSubmit={form.handleSubmit(handleSubmitForm)} variant='outline'
className='space-y-4' onClick={() => setShowDialog(false)}
> >
<FormField {t('Cancel')}
control={form.control} </Button>
name='question' <Button type='submit' form={FAQ_FORM_ID}>
render={({ field }) => ( {editingFaq ? t('Update') : t('Add')}
<FormItem> </Button>
<FormLabel>{t('Question')}</FormLabel> </>
<FormControl> }
<Input >
placeholder={t('How to reset my quota?')} <Form {...form}>
{...field} <form
/> id={FAQ_FORM_ID}
</FormControl> onSubmit={form.handleSubmit(handleSubmitForm)}
<FormDescription> className='space-y-4'
{t('Maximum 200 characters')} >
</FormDescription> <FormField
<FormMessage /> control={form.control}
</FormItem> name='question'
)} render={({ field }) => (
/> <FormItem>
<FormField <FormLabel>{t('Question')}</FormLabel>
control={form.control} <FormControl>
name='answer' <Input
render={({ field }) => ( placeholder={t('How to reset my quota?')}
<FormItem> {...field}
<FormLabel>{t('Answer')}</FormLabel> />
<FormControl> </FormControl>
<Textarea <FormDescription>
placeholder={t( {t('Maximum 200 characters')}
'Visit Settings → General and adjust quota options...' </FormDescription>
)} <FormMessage />
rows={8} </FormItem>
{...field} )}
/> />
</FormControl> <FormField
<FormDescription> control={form.control}
{t( name='answer'
'Maximum 1000 characters. Supports Markdown and HTML.' render={({ field }) => (
<FormItem>
<FormLabel>{t('Answer')}</FormLabel>
<FormControl>
<Textarea
placeholder={t(
'Visit Settings → General and adjust quota options...'
)} )}
</FormDescription> rows={8}
<FormMessage /> {...field}
</FormItem> />
)} </FormControl>
/> <FormDescription>
<DialogFooter> {t('Maximum 1000 characters. Supports Markdown and HTML.')}
<Button </FormDescription>
type='button' <FormMessage />
variant='outline' </FormItem>
onClick={() => setShowDialog(false)} )}
> />
{t('Cancel')} </form>
</Button> </Form>
<Button type='submit'>
{editingFaq ? t('Update') : t('Add')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog> </Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
...@@ -36,14 +36,6 @@ import { ...@@ -36,14 +36,6 @@ import {
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -61,6 +53,7 @@ import { ...@@ -61,6 +53,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { Dialog } from '@/components/dialog'
import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSwitchField } from '../components/settings-form-layout'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
...@@ -97,6 +90,8 @@ const createUptimeKumaSchema = (t: (key: string) => string) => ...@@ -97,6 +90,8 @@ const createUptimeKumaSchema = (t: (key: string) => string) =>
type UptimeKumaFormValues = z.infer<ReturnType<typeof createUptimeKumaSchema>> type UptimeKumaFormValues = z.infer<ReturnType<typeof createUptimeKumaSchema>>
const UPTIME_KUMA_FORM_ID = 'uptime-kuma-form'
export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const updateOption = useUpdateOption() const updateOption = useUpdateOption()
...@@ -359,96 +354,100 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { ...@@ -359,96 +354,100 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
</div> </div>
</div> </div>
<Dialog open={showDialog} onOpenChange={setShowDialog}> <Dialog
<DialogContent> open={showDialog}
<DialogHeader> onOpenChange={setShowDialog}
<DialogTitle> title={
{editingGroup editingGroup
? t('Edit Uptime Kuma Group') ? t('Edit Uptime Kuma Group')
: t('Add Uptime Kuma Group')} : t('Add Uptime Kuma Group')
</DialogTitle> }
<DialogDescription> description={t(
{t('Configure monitoring status page groups for the dashboard')} 'Configure monitoring status page groups for the dashboard'
</DialogDescription> )}
</DialogHeader> contentHeight='auto'
<Form {...form}> bodyClassName='space-y-4'
<form footer={
onSubmit={form.handleSubmit(handleSubmitForm)} <>
className='space-y-4' <Button
type='button'
variant='outline'
onClick={() => setShowDialog(false)}
> >
<FormField {t('Cancel')}
control={form.control} </Button>
name='categoryName' <Button type='submit' form={UPTIME_KUMA_FORM_ID}>
render={({ field }) => ( {editingGroup ? t('Update') : t('Add')}
<FormItem> </Button>
<FormLabel>{t('Category Name')}</FormLabel> </>
<FormControl> }
<Input >
placeholder={t('e.g., Core APIs, OpenAI, Claude')} <Form {...form}>
{...field} <form
/> id={UPTIME_KUMA_FORM_ID}
</FormControl> onSubmit={form.handleSubmit(handleSubmitForm)}
<FormDescription> className='space-y-4'
{t( >
'Display name for this monitoring group (max 50 characters)' <FormField
)} control={form.control}
</FormDescription> name='categoryName'
<FormMessage /> render={({ field }) => (
</FormItem> <FormItem>
)} <FormLabel>{t('Category Name')}</FormLabel>
/> <FormControl>
<FormField <Input
control={form.control} placeholder={t('e.g., Core APIs, OpenAI, Claude')}
name='url' {...field}
render={({ field }) => ( />
<FormItem> </FormControl>
<FormLabel>{t('Uptime Kuma URL')}</FormLabel> <FormDescription>
<FormControl> {t(
<Input 'Display name for this monitoring group (max 50 characters)'
placeholder={t('https://status.example.com')} )}
{...field} </FormDescription>
/> <FormMessage />
</FormControl> </FormItem>
<FormDescription> )}
{t('Base URL of your Uptime Kuma instance')} />
</FormDescription> <FormField
<FormMessage /> control={form.control}
</FormItem> name='url'
)} render={({ field }) => (
/> <FormItem>
<FormField <FormLabel>{t('Uptime Kuma URL')}</FormLabel>
control={form.control} <FormControl>
name='slug' <Input
render={({ field }) => ( placeholder={t('https://status.example.com')}
<FormItem> {...field}
<FormLabel>{t('Status Page Slug')}</FormLabel> />
<FormControl> </FormControl>
<Input placeholder={t('my-status')} {...field} /> <FormDescription>
</FormControl> {t('Base URL of your Uptime Kuma instance')}
<FormDescription> </FormDescription>
{t('The slug is appended to the URL:')} {'{url}'} <FormMessage />
{t('/status/')} </FormItem>
{'{slug}'} )}
</FormDescription> />
<FormMessage /> <FormField
</FormItem> control={form.control}
)} name='slug'
/> render={({ field }) => (
<DialogFooter> <FormItem>
<Button <FormLabel>{t('Status Page Slug')}</FormLabel>
type='button' <FormControl>
variant='outline' <Input placeholder={t('my-status')} {...field} />
onClick={() => setShowDialog(false)} </FormControl>
> <FormDescription>
{t('Cancel')} {t('The slug is appended to the URL:')} {'{url}'}
</Button> {t('/status/')}
<Button type='submit'> {'{slug}'}
{editingGroup ? t('Update') : t('Add')} </FormDescription>
</Button> <FormMessage />
</DialogFooter> </FormItem>
</form> )}
</Form> />
</DialogContent> </form>
</Form>
</Dialog> </Dialog>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment