new-api/web/default/src/features/system-settings/integrations/amount-options-visual-editor.tsx
t0ng7u 948780e3fa 🎨 fix(theme): align UI controls with global radius tokens
Remove hard-coded and capped border radius overrides so shared controls and feature actions consistently follow the active theme radius.

- Replace fixed radius utilities with semantic theme-aware radius tokens
- Remove redundant `rounded-full` and pixel-based overrides from header, toolbar, Playground, and utility actions
- Drop unused `StatusBadge` rounded prop usage
- Keep existing component behavior intact while improving global theme consistency
2026-05-08 01:50:03 +08:00

149 lines
4.4 KiB
TypeScript
Vendored

import { useState, useMemo } from 'react'
import { Plus, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { StatusBadge } from '@/components/status-badge'
import { safeJsonParseWithValidation } from '../utils/json-parser'
import { isArray } from '../utils/json-validators'
type AmountOptionsVisualEditorProps = {
value: string
onChange: (value: string) => void
}
export function AmountOptionsVisualEditor({
value,
onChange,
}: AmountOptionsVisualEditorProps) {
const { t } = useTranslation()
const [newAmount, setNewAmount] = useState('')
const amounts = useMemo(() => {
const parsed = safeJsonParseWithValidation<unknown[]>(value, {
fallback: [],
validator: isArray,
validatorMessage: t('Amount options must be a JSON array'),
context: 'amount options',
})
return parsed
.filter((item) => typeof item === 'number' || !isNaN(Number(item)))
.map(Number)
.sort((a, b) => a - b)
}, [value, t])
const handleAdd = () => {
const amount = parseFloat(newAmount)
if (isNaN(amount) || amount <= 0) {
return
}
try {
const updatedAmounts = [...amounts, amount]
.filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
.sort((a, b) => a - b)
onChange(JSON.stringify(updatedAmounts, null, 2))
setNewAmount('')
} catch (_error) {
// eslint-disable-next-line no-console
console.error('Failed to add amount:', _error)
}
}
const handleRemove = (amount: number) => {
try {
const updatedAmounts = amounts.filter((a) => a !== amount)
onChange(JSON.stringify(updatedAmounts, null, 2))
} catch (_error) {
// eslint-disable-next-line no-console
console.error('Failed to remove amount:', _error)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAdd()
}
}
return (
<div className='space-y-4'>
<div>
<p className='text-muted-foreground mb-3 text-sm'>
{t('Preset recharge amounts displayed to users')}
</p>
{amounts.length === 0 ? (
<div className='text-muted-foreground rounded-lg border border-dashed p-6 text-center text-sm'>
{t(
'No amount options configured. Add amounts below to get started.'
)}
</div>
) : (
<div className='flex flex-wrap gap-2'>
{amounts.map((amount) => (
<StatusBadge
key={amount}
variant='neutral'
className='text-base'
copyable={false}
>
<span className='font-mono'>${amount}</span>
<Button
type='button'
variant='ghost'
size='icon-sm'
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleRemove(amount)
}}
className='hover:bg-muted-foreground/20 size-auto p-0.5'
aria-label={t('Remove ${{amount}}', { amount })}
>
<X className='h-3.5 w-3.5' />
</Button>
</StatusBadge>
))}
</div>
)}
</div>
<div className='flex flex-col gap-2 sm:flex-row sm:items-end'>
<div className='flex-1'>
<Label htmlFor='new-amount' className='mb-2 block'>
{t('Add new amount')}
</Label>
<Input
id='new-amount'
type='number'
step='0.01'
min='0'
placeholder={t('e.g., 100')}
value={newAmount}
onChange={(e) => setNewAmount(e.target.value)}
onKeyDown={handleKeyDown}
/>
</div>
<Button
type='button'
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleAdd()
}}
disabled={!newAmount || parseFloat(newAmount) <= 0}
className='w-full sm:w-auto'
>
<Plus className='h-4 w-4 sm:mr-2' />
<span className='sm:inline'>{t('Add')}</span>
</Button>
</div>
</div>
)
}