style(ui): align wallet and profile interactions
This commit is contained in:
@@ -16,7 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { flexRender, type Header, type Table as TanstackTable } from '@tanstack/react-table'
|
||||
import {
|
||||
flexRender,
|
||||
type Header,
|
||||
type Table as TanstackTable,
|
||||
} from '@tanstack/react-table'
|
||||
import { TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { DataTableColumnHeader } from './column-header'
|
||||
import type { DataTableColumnClassName } from './types'
|
||||
|
||||
@@ -344,7 +344,8 @@ function renderDesktop<TData>(
|
||||
splitHeader={fixedHeight}
|
||||
tableContainerClassName={fixedHeight ? 'h-full min-h-0' : undefined}
|
||||
tableHeaderClassName={cn(
|
||||
fixedHeight && '[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
|
||||
fixedHeight &&
|
||||
'[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
|
||||
props.tableHeaderClassName
|
||||
)}
|
||||
getColumnClassName={props.getColumnClassName}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from '@tanstack/react-table'
|
||||
import { Database } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StatusBadgeTypeContext } from '@/components/status-badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Empty,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { StatusBadgeTypeContext } from '@/components/status-badge'
|
||||
|
||||
interface MobileCardListProps<TData> {
|
||||
table: Table<TData>
|
||||
@@ -211,7 +211,10 @@ function FallbackRow<TData>({ row }: { row: Row<TData> }) {
|
||||
|
||||
if (!label) {
|
||||
return (
|
||||
<div key={cell.id} className='flex justify-end overflow-hidden [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'>
|
||||
<div
|
||||
key={cell.id}
|
||||
className='flex justify-end overflow-hidden [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'
|
||||
>
|
||||
<StatusBadgeTypeContext.Provider value='text'>
|
||||
{renderCellContent(cell)}
|
||||
</StatusBadgeTypeContext.Provider>
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ export const staticDataTableClassNames = {
|
||||
embeddedContainer: 'rounded-none border-0',
|
||||
compactTable: 'text-sm',
|
||||
compactHeaderRow: 'hover:bg-transparent',
|
||||
mutedHeaderRow: '[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))] hover:[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
|
||||
mutedHeaderRow:
|
||||
'[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))] hover:[background-color:color-mix(in_oklch,var(--muted)_30%,var(--background))]',
|
||||
compactHeaderCell:
|
||||
'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase',
|
||||
compactHeaderCellRight:
|
||||
|
||||
+2
-8
@@ -38,20 +38,14 @@ export function ProviderBadge({
|
||||
return (
|
||||
<div
|
||||
data-slot='provider-badge'
|
||||
className={cn(
|
||||
'flex min-w-0 max-w-full items-center gap-1.5',
|
||||
className
|
||||
)}
|
||||
className={cn('flex max-w-full min-w-0 items-center gap-1.5', className)}
|
||||
>
|
||||
{icon && <span className='flex shrink-0 items-center'>{icon}</span>}
|
||||
<StatusBadge
|
||||
label={label}
|
||||
autoColor={label}
|
||||
size='sm'
|
||||
className={cn(
|
||||
'min-w-0 shrink overflow-hidden',
|
||||
!icon && 'pl-0'
|
||||
)}
|
||||
className={cn('min-w-0 shrink overflow-hidden', !icon && 'pl-0')}
|
||||
{...badgeProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
<tr
|
||||
data-slot='table-row'
|
||||
className={cn(
|
||||
'group hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] has-aria-expanded:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] data-[state=selected]:bg-muted border-b transition-colors',
|
||||
'group data-[state=selected]:bg-muted border-b transition-colors hover:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))] has-aria-expanded:[background-color:color-mix(in_oklch,var(--muted)_50%,var(--background))]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+6
-1
@@ -32,6 +32,7 @@ type TitledCardProps = {
|
||||
icon?: ReactNode
|
||||
action?: ReactNode
|
||||
children?: ReactNode
|
||||
disableHoverEffect?: boolean
|
||||
className?: string
|
||||
headerClassName?: string
|
||||
contentClassName?: string
|
||||
@@ -46,6 +47,7 @@ export function TitledCard({
|
||||
icon,
|
||||
action,
|
||||
children,
|
||||
disableHoverEffect,
|
||||
className,
|
||||
headerClassName,
|
||||
contentClassName,
|
||||
@@ -54,7 +56,10 @@ export function TitledCard({
|
||||
descriptionClassName,
|
||||
}: TitledCardProps) {
|
||||
return (
|
||||
<Card className={cn('gap-0 overflow-hidden py-0', className)}>
|
||||
<Card
|
||||
data-card-hover={disableHoverEffect ? 'false' : undefined}
|
||||
className={cn('gap-0 overflow-hidden py-0', className)}
|
||||
>
|
||||
<CardHeader
|
||||
className={cn('border-b p-3 !pb-3 sm:p-5 sm:!pb-5', headerClassName)}
|
||||
>
|
||||
|
||||
@@ -622,7 +622,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 max-w-full items-center gap-2 overflow-hidden'>
|
||||
<div className='flex max-w-full min-w-0 items-center gap-2 overflow-hidden'>
|
||||
{isMultiKey && (
|
||||
<TooltipProvider delay={100}>
|
||||
<Tooltip>
|
||||
@@ -641,7 +641,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<div className='min-w-0 max-w-full overflow-hidden' />
|
||||
<div className='max-w-full min-w-0 overflow-hidden' />
|
||||
}
|
||||
>
|
||||
<ProviderBadge
|
||||
@@ -649,7 +649,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
label={typeName}
|
||||
copyable={false}
|
||||
showDot={false}
|
||||
className='min-w-0 max-w-full overflow-hidden'
|
||||
className='max-w-full min-w-0 overflow-hidden'
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>{typeName}</TooltipContent>
|
||||
@@ -895,7 +895,14 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
if (!tag)
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
|
||||
return <StatusBadge label={tag} autoColor={tag} size='sm' className='-ml-1.5' />
|
||||
return (
|
||||
<StatusBadge
|
||||
label={tag}
|
||||
autoColor={tag}
|
||||
size='sm'
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 120,
|
||||
enableSorting: false,
|
||||
|
||||
+4
-2
@@ -190,7 +190,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) {
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'text-foreground/5 pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono sm:block dark:text-foreground/8',
|
||||
'text-foreground/5 dark:text-foreground/8 pointer-events-none absolute inset-y-0 right-0 hidden overflow-hidden font-mono sm:block',
|
||||
props.compact ? 'w-1/2 opacity-45' : 'w-[58%] opacity-75'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
@@ -589,7 +589,9 @@ export function OverviewDashboard() {
|
||||
model,
|
||||
keyName,
|
||||
keyId: preferredKey?.id,
|
||||
displayKey: preferredKey ? formatDisplayKey(`sk-${preferredKey.key}`) : 'sk-...',
|
||||
displayKey: preferredKey
|
||||
? formatDisplayKey(`sk-${preferredKey.key}`)
|
||||
: 'sk-...',
|
||||
ready,
|
||||
}
|
||||
}, [apiInfoItems, modelsQuery.data, preferredKey, t])
|
||||
|
||||
@@ -64,77 +64,77 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
return (
|
||||
<div className='-ml-2'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Open menu')}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-48'>
|
||||
{/* Edit */}
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
{t('Edit')}
|
||||
<DropdownMenuShortcut>
|
||||
<Pencil size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Enable/Disable */}
|
||||
<DropdownMenuItem onClick={handleToggleStatus}>
|
||||
{isEnabled ? (
|
||||
<>
|
||||
{t('Disable')}
|
||||
<DropdownMenuShortcut>
|
||||
<PowerOff size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Enable')}
|
||||
<DropdownMenuShortcut>
|
||||
<Power size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Delete */}
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
setDeleteConfirmOpen(true)
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('Delete')}
|
||||
<DropdownMenuShortcut>
|
||||
<Trash2 size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Open menu')}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-48'>
|
||||
{/* Edit */}
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
{t('Edit')}
|
||||
<DropdownMenuShortcut>
|
||||
<Pencil size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onOpenChange={setDeleteConfirmOpen}
|
||||
title={t('Delete Model')}
|
||||
desc={`Are you sure you want to delete "${model.model_name}"? This action cannot be undone.`}
|
||||
confirmText='Delete'
|
||||
destructive
|
||||
handleConfirm={() => {
|
||||
handleDeleteModel(model.id, queryClient)
|
||||
setDeleteConfirmOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Enable/Disable */}
|
||||
<DropdownMenuItem onClick={handleToggleStatus}>
|
||||
{isEnabled ? (
|
||||
<>
|
||||
{t('Disable')}
|
||||
<DropdownMenuShortcut>
|
||||
<PowerOff size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Enable')}
|
||||
<DropdownMenuShortcut>
|
||||
<Power size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Delete */}
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
setDeleteConfirmOpen(true)
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
>
|
||||
{t('Delete')}
|
||||
<DropdownMenuShortcut>
|
||||
<Trash2 size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onOpenChange={setDeleteConfirmOpen}
|
||||
title={t('Delete Model')}
|
||||
desc={`Are you sure you want to delete "${model.model_name}"? This action cannot be undone.`}
|
||||
confirmText='Delete'
|
||||
destructive
|
||||
handleConfirm={() => {
|
||||
handleDeleteModel(model.id, queryClient)
|
||||
setDeleteConfirmOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -322,13 +322,11 @@ export function DynamicPricingBreakdown({
|
||||
className: 'text-muted-foreground py-2 font-medium',
|
||||
cellClassName: 'py-2.5 align-top',
|
||||
cell: (tier) => {
|
||||
const condSummary = formatConditionSummary(
|
||||
tier.conditions,
|
||||
t
|
||||
)
|
||||
const condSummary = formatConditionSummary(tier.conditions, t)
|
||||
const isMatched =
|
||||
normalizedMatchedTierLabel !== '' &&
|
||||
normalizeTierLabel(tier.label) === normalizedMatchedTierLabel
|
||||
normalizeTierLabel(tier.label) ===
|
||||
normalizedMatchedTierLabel
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-wrap items-center gap-1.5'>
|
||||
|
||||
@@ -57,9 +57,7 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
|
||||
const groups = props.model.enable_groups || []
|
||||
const endpoints = props.model.supported_endpoint_types || []
|
||||
const modelIconKey = props.model.icon || props.model.vendor_icon
|
||||
const modelIcon = modelIconKey
|
||||
? getLobeIcon(modelIconKey, 28)
|
||||
: null
|
||||
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 28) : null
|
||||
const initial = props.model.model_name?.charAt(0).toUpperCase() || '?'
|
||||
const isDynamicPricing =
|
||||
props.model.billing_mode === 'tiered_expr' &&
|
||||
|
||||
@@ -32,6 +32,7 @@ import { formatQuotaWithCurrency } from '@/lib/currency'
|
||||
import dayjs from '@/lib/dayjs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -221,7 +222,7 @@ export function CheckinCalendarCard({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='bg-card overflow-hidden rounded-2xl border'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<div className='p-6'>
|
||||
<div className='flex items-start justify-between gap-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
@@ -234,7 +235,7 @@ export function CheckinCalendarCard({
|
||||
<Skeleton className='h-9 w-28 rounded-md' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,14 +271,13 @@ export function CheckinCalendarCard({
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<div className='bg-card overflow-hidden rounded-2xl border'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
{/* Header */}
|
||||
<div className='border-b p-4 sm:p-6'>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4'>
|
||||
<Button
|
||||
<button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
className='flex h-auto min-w-0 flex-1 items-start gap-3 p-0 text-left whitespace-normal hover:bg-transparent'
|
||||
className='flex min-w-0 flex-1 items-start gap-3 rounded-lg text-left whitespace-normal outline-none'
|
||||
onClick={() => setCollapsed((v) => !v)}
|
||||
>
|
||||
<div className='bg-primary/10 text-primary flex h-10 w-10 shrink-0 items-center justify-center rounded-xl sm:h-11 sm:w-11'>
|
||||
@@ -311,7 +311,7 @@ export function CheckinCalendarCard({
|
||||
: t('Check in daily to receive random quota rewards')}
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
</button>
|
||||
<Button
|
||||
onClick={() => doCheckin()}
|
||||
disabled={checkinLoading || checkedToday}
|
||||
@@ -423,7 +423,6 @@ export function CheckinCalendarCard({
|
||||
'relative flex h-9 w-full flex-col items-center justify-center rounded-lg px-0 text-xs font-medium sm:h-10 sm:text-sm',
|
||||
!dayObj.isCurrentMonth &&
|
||||
'text-muted-foreground/40 cursor-default',
|
||||
isToday && 'hover:bg-primary/90',
|
||||
!isToday && isCheckedIn && 'font-semibold'
|
||||
)}
|
||||
>
|
||||
@@ -476,7 +475,7 @@ export function CheckinCalendarCard({
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
title={t('Language Preferences')}
|
||||
description={t('Set the language used across the interface')}
|
||||
icon={<Languages className='h-4 w-4' />}
|
||||
disableHoverEffect
|
||||
>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
|
||||
<div className='space-y-1'>
|
||||
|
||||
@@ -187,7 +187,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
||||
|
||||
if (pageLoading || loading) {
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='p-3 sm:p-5'>
|
||||
<Skeleton className='h-6 w-48' />
|
||||
<Skeleton className='mt-2 h-4 w-64' />
|
||||
@@ -208,7 +208,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='p-3 sm:p-5'>
|
||||
<CardTitle className='text-lg tracking-tight sm:text-xl'>
|
||||
{t('Passkey Login')}
|
||||
@@ -310,7 +310,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
|
||||
{t('Cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
className='bg-destructive text-destructive-foreground'
|
||||
disabled={removing}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
+19
-12
@@ -18,12 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { Activity, BarChart3, WalletCards } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
||||
import { formatCompactNumber, formatQuota } from '@/lib/format'
|
||||
import { getRoleLabel } from '@/lib/roles'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { getUserInitials, getDisplayName } from '../lib'
|
||||
import { getDisplayName } from '../lib'
|
||||
import type { UserProfile } from '../types'
|
||||
|
||||
// ============================================================================
|
||||
@@ -40,8 +42,8 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='bg-card overflow-hidden rounded-lg border'>
|
||||
<div className='p-4 sm:p-5'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardContent className='p-4 sm:p-5'>
|
||||
<div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
|
||||
<Skeleton className='h-16 w-16 rounded-2xl' />
|
||||
<div className='space-y-3'>
|
||||
@@ -56,7 +58,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='border-t'>
|
||||
<div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
@@ -68,14 +70,16 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (!profile) return null
|
||||
|
||||
const displayName = getDisplayName(profile)
|
||||
const initials = getUserInitials(profile)
|
||||
const avatarName = profile.username || displayName
|
||||
const avatarFallback = getUserAvatarFallback(avatarName)
|
||||
const avatarFallbackStyle = getUserAvatarStyle(avatarName)
|
||||
const roleLabel = getRoleLabel(profile.role)
|
||||
const stats = [
|
||||
{
|
||||
@@ -99,12 +103,15 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='bg-card overflow-hidden rounded-lg border'>
|
||||
<div className='p-3 sm:p-5'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardContent className='p-3 sm:p-5'>
|
||||
<div className='flex items-center gap-3 text-left sm:gap-4'>
|
||||
<Avatar className='ring-background h-12 w-12 rounded-xl text-sm ring-2 sm:h-16 sm:w-16 sm:rounded-2xl sm:text-lg sm:ring-4'>
|
||||
<AvatarFallback className='bg-primary/10 text-primary rounded-xl sm:rounded-2xl'>
|
||||
{initials}
|
||||
<AvatarFallback
|
||||
className='rounded-xl font-semibold text-white sm:rounded-2xl'
|
||||
style={avatarFallbackStyle}
|
||||
>
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
@@ -142,7 +149,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className='border-t'>
|
||||
<div className='divide-border/60 grid grid-cols-3 divide-x'>
|
||||
{stats.map((item) => (
|
||||
@@ -164,6 +171,6 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProfileSecurityCard({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
|
||||
<Skeleton className='h-6 w-32' />
|
||||
<Skeleton className='mt-2 h-4 w-48' />
|
||||
@@ -93,6 +93,7 @@ export function ProfileSecurityCard({
|
||||
title={t('Security')}
|
||||
description={t('Manage your security settings and account access')}
|
||||
icon={<Shield className='h-4 w-4' />}
|
||||
disableHoverEffect
|
||||
>
|
||||
<div className='grid grid-cols-1 gap-2.5 sm:gap-3 md:grid-cols-3'>
|
||||
{securityActions.map((item) => (
|
||||
@@ -100,10 +101,8 @@ export function ProfileSecurityCard({
|
||||
key={item.title}
|
||||
type='button'
|
||||
onClick={item.action}
|
||||
className={`hover:bg-muted/50 flex items-center gap-3 rounded-lg border p-3 text-left transition-colors md:flex-col md:gap-2 md:p-4 md:text-center ${
|
||||
item.variant === 'destructive'
|
||||
? 'border-destructive/30 hover:border-destructive/50 hover:bg-destructive/5'
|
||||
: ''
|
||||
className={`flex items-center gap-3 rounded-lg border p-3 text-left md:flex-col md:gap-2 md:p-4 md:text-center ${
|
||||
item.variant === 'destructive' ? 'border-destructive/30' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProfileSettingsCard({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
|
||||
<Skeleton className='h-6 w-32' />
|
||||
<Skeleton className='mt-2 h-4 w-48' />
|
||||
@@ -67,6 +67,7 @@ export function ProfileSettingsCard({
|
||||
title={t('Settings')}
|
||||
description={t('Configure your account preferences and integrations')}
|
||||
icon={<Settings className='h-4 w-4' />}
|
||||
disableHoverEffect
|
||||
>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className='grid w-full grid-cols-2 items-stretch gap-1 rounded-xl p-1 group-data-horizontal/tabs:h-10'>
|
||||
|
||||
@@ -200,7 +200,7 @@ export function SidebarModulesCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-9 sm:w-9'>
|
||||
@@ -240,7 +240,7 @@ export function SidebarModulesCard() {
|
||||
{section.modules.map((mod) => (
|
||||
<div
|
||||
key={mod.key}
|
||||
className={`flex min-h-16 items-center justify-between rounded-lg border p-3 transition-opacity ${
|
||||
className={`flex min-h-16 items-center justify-between rounded-lg border p-3 ${
|
||||
sectionEnabled ? '' : 'opacity-50'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -350,7 +350,7 @@ export function AccountBindingsTab({
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='text-destructive hover:text-destructive h-7 shrink-0 px-2.5 text-xs'
|
||||
className='text-destructive h-7 shrink-0 px-2.5 text-xs'
|
||||
onClick={() => setUnbindTarget(binding)}
|
||||
>
|
||||
<Unlink className='mr-1 h-3 w-3' />
|
||||
|
||||
@@ -24,8 +24,8 @@ import { ROLE } from '@/lib/roles'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { PasswordInput } from '@/components/password-input'
|
||||
import { updateUserSettings } from '../../api'
|
||||
import {
|
||||
@@ -35,13 +35,24 @@ import {
|
||||
import { parseUserSettings } from '../../lib'
|
||||
import type { UserProfile, UserSettings, NotifyType } from '../../types'
|
||||
|
||||
const NOTIFICATION_ICONS: Record<string, typeof Mail> = {
|
||||
const NOTIFICATION_ICONS: Record<NotifyType, typeof Mail> = {
|
||||
email: Mail,
|
||||
webhook: Webhook,
|
||||
bark: Bell,
|
||||
gotify: Server,
|
||||
}
|
||||
|
||||
const NOTIFICATION_VALUES = new Set<NotifyType>(
|
||||
NOTIFICATION_METHODS.map((method) => method.value)
|
||||
)
|
||||
|
||||
function normalizeNotifyType(value: unknown): NotifyType {
|
||||
return typeof value === 'string' &&
|
||||
NOTIFICATION_VALUES.has(value as NotifyType)
|
||||
? (value as NotifyType)
|
||||
: 'email'
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Settings Tab Component
|
||||
// ============================================================================
|
||||
@@ -82,7 +93,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
if (profile?.setting) {
|
||||
const parsed = parseUserSettings(profile.setting)
|
||||
setSettings({
|
||||
notify_type: parsed.notify_type || 'email',
|
||||
notify_type: normalizeNotifyType(parsed.notify_type),
|
||||
quota_warning_threshold:
|
||||
parsed.quota_warning_threshold ?? DEFAULT_QUOTA_WARNING_THRESHOLD,
|
||||
notification_email: parsed.notification_email ?? '',
|
||||
@@ -119,44 +130,42 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const notifyType = normalizeNotifyType(settings.notify_type)
|
||||
|
||||
return (
|
||||
<div className='space-y-4 sm:space-y-6'>
|
||||
{/* Notification Type */}
|
||||
<div className='space-y-2.5'>
|
||||
<Label>{t('Notification Method')}</Label>
|
||||
<RadioGroup
|
||||
value={settings.notify_type}
|
||||
onValueChange={(value) =>
|
||||
updateField('notify_type', value as NotifyType)
|
||||
}
|
||||
className='grid grid-cols-4 gap-1.5 sm:gap-3'
|
||||
<ToggleGroup
|
||||
value={[notifyType]}
|
||||
onValueChange={(value) => {
|
||||
const nextValue = value.find((item) => item !== notifyType)
|
||||
if (nextValue)
|
||||
updateField('notify_type', normalizeNotifyType(nextValue))
|
||||
}}
|
||||
aria-label={t('Notification Method')}
|
||||
variant='outline'
|
||||
size='lg'
|
||||
spacing={2}
|
||||
className='grid w-full grid-cols-2 gap-2 sm:grid-cols-4 sm:gap-3'
|
||||
>
|
||||
{NOTIFICATION_METHODS.map((method) => {
|
||||
const Icon = NOTIFICATION_ICONS[method.value]
|
||||
const isSelected = settings.notify_type === method.value
|
||||
return (
|
||||
<Label
|
||||
<ToggleGroupItem
|
||||
key={method.value}
|
||||
htmlFor={method.value}
|
||||
className={`flex min-h-16 cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border p-2 text-center transition-colors sm:min-h-20 sm:gap-2 sm:border-2 sm:p-3 ${
|
||||
isSelected
|
||||
? 'border-primary bg-primary/5 text-primary'
|
||||
: 'border-muted hover:border-muted-foreground/25 hover:bg-muted/50'
|
||||
}`}
|
||||
value={method.value}
|
||||
className='h-auto min-h-14 w-full flex-col gap-1.5 px-3 py-3 sm:min-h-16'
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={method.value}
|
||||
id={method.value}
|
||||
className='sr-only'
|
||||
/>
|
||||
<Icon className='h-4 w-4 sm:h-5 sm:w-5' />
|
||||
<span className='max-w-full truncate text-xs font-medium sm:text-sm'>
|
||||
{t(method.label)}
|
||||
</span>
|
||||
</Label>
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
})}
|
||||
</RadioGroup>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Warning Threshold */}
|
||||
@@ -178,7 +187,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
</div>
|
||||
|
||||
{/* Email Settings */}
|
||||
{settings.notify_type === 'email' && (
|
||||
{notifyType === 'email' && (
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='notifyEmail'>{t('Notification Email')}</Label>
|
||||
<Input
|
||||
@@ -193,7 +202,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
)}
|
||||
|
||||
{/* Webhook Settings */}
|
||||
{settings.notify_type === 'webhook' && (
|
||||
{notifyType === 'webhook' && (
|
||||
<>
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='webhookUrl'>{t('Webhook URL')}</Label>
|
||||
@@ -219,7 +228,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
)}
|
||||
|
||||
{/* Bark Settings */}
|
||||
{settings.notify_type === 'bark' && (
|
||||
{notifyType === 'bark' && (
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='barkUrl'>{t('Bark Push URL')}</Label>
|
||||
<Input
|
||||
@@ -237,7 +246,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
)}
|
||||
|
||||
{/* Gotify Settings */}
|
||||
{settings.notify_type === 'gotify' && (
|
||||
{notifyType === 'gotify' && (
|
||||
<>
|
||||
<div className='space-y-1.5'>
|
||||
<Label htmlFor='gotifyUrl'>{t('Gotify Server URL')}</Label>
|
||||
@@ -300,7 +309,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
|
||||
href='https://gotify.net/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-primary hover:underline'
|
||||
className='text-primary underline underline-offset-4'
|
||||
>
|
||||
{t('Gotify Documentation')}
|
||||
</a>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
|
||||
|
||||
if (pageLoading || loading) {
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='p-3 sm:p-5'>
|
||||
<Skeleton className='h-6 w-48' />
|
||||
<Skeleton className='mt-2 h-4 w-64' />
|
||||
@@ -65,7 +65,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='p-3 sm:p-5'>
|
||||
<CardTitle className='text-lg tracking-tight sm:text-xl'>
|
||||
{t('Two-Factor Authentication')}
|
||||
|
||||
+56
-56
@@ -79,64 +79,64 @@ export function DataTableRowActions<TData>({
|
||||
return (
|
||||
<div className='-ml-2'>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DotsHorizontalIcon className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Open menu')}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-[160px]'>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setCurrentRow(redemption)
|
||||
setOpen('update')
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('Edit')}
|
||||
<DropdownMenuShortcut>
|
||||
<Edit size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
{canToggle && (
|
||||
<DropdownMenuItem onClick={handleToggleStatus}>
|
||||
{isEnabled ? (
|
||||
<>
|
||||
{t('Disable')}
|
||||
<DropdownMenuShortcut>
|
||||
<PowerOff size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Enable')}
|
||||
<DropdownMenuShortcut>
|
||||
<Power size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
)}
|
||||
<DotsHorizontalIcon className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Open menu')}</span>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-[160px]'>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setCurrentRow(redemption)
|
||||
setOpen('update')
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{t('Edit')}
|
||||
<DropdownMenuShortcut>
|
||||
<Edit size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setCurrentRow(redemption)
|
||||
setOpen('delete')
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
>
|
||||
{t('Delete')}
|
||||
<DropdownMenuShortcut>
|
||||
<Trash2 size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{canToggle && (
|
||||
<DropdownMenuItem onClick={handleToggleStatus}>
|
||||
{isEnabled ? (
|
||||
<>
|
||||
{t('Disable')}
|
||||
<DropdownMenuShortcut>
|
||||
<PowerOff size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Enable')}
|
||||
<DropdownMenuShortcut>
|
||||
<Power size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setCurrentRow(redemption)
|
||||
setOpen('delete')
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
>
|
||||
{t('Delete')}
|
||||
<DropdownMenuShortcut>
|
||||
<Trash2 size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+36
-36
@@ -40,43 +40,43 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
return (
|
||||
<div className='-ml-2'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant='ghost' className='h-8 w-8 p-0' />}
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem
|
||||
disabled={!complianceConfirmed}
|
||||
onClick={() => {
|
||||
setCurrentRow(row.original)
|
||||
setOpen('update')
|
||||
}}
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant='ghost' className='h-8 w-8 p-0' />}
|
||||
>
|
||||
<Pencil className='mr-2 h-4 w-4' />
|
||||
{t('Edit')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!complianceConfirmed}
|
||||
onClick={() => {
|
||||
setCurrentRow(row.original)
|
||||
setOpen('toggle-status')
|
||||
}}
|
||||
>
|
||||
{row.original.plan.enabled ? (
|
||||
<>
|
||||
<PowerOff className='mr-2 h-4 w-4' />
|
||||
{t('Disable')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className='mr-2 h-4 w-4' />
|
||||
{t('Enable')}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem
|
||||
disabled={!complianceConfirmed}
|
||||
onClick={() => {
|
||||
setCurrentRow(row.original)
|
||||
setOpen('update')
|
||||
}}
|
||||
>
|
||||
<Pencil className='mr-2 h-4 w-4' />
|
||||
{t('Edit')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={!complianceConfirmed}
|
||||
onClick={() => {
|
||||
setCurrentRow(row.original)
|
||||
setOpen('toggle-status')
|
||||
}}
|
||||
>
|
||||
{row.original.plan.enabled ? (
|
||||
<>
|
||||
<PowerOff className='mr-2 h-4 w-4' />
|
||||
{t('Disable')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className='mr-2 h-4 w-4' />
|
||||
{t('Enable')}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -307,8 +307,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
|
||||
{
|
||||
id: 'url',
|
||||
header: t('Uptime Kuma URL'),
|
||||
cellClassName:
|
||||
'text-primary max-w-xs truncate font-mono text-sm',
|
||||
cellClassName: 'text-primary max-w-xs truncate font-mono text-sm',
|
||||
cell: (group) => group.url,
|
||||
},
|
||||
{
|
||||
|
||||
+3
-1
@@ -554,7 +554,9 @@ export function ChannelAffinitySection(props: Props) {
|
||||
{
|
||||
id: 'model-regex',
|
||||
header: t('Model Regex'),
|
||||
cell: (rule) => <RuleBadgeList items={rule.model_regex || []} />,
|
||||
cell: (rule) => (
|
||||
<RuleBadgeList items={rule.model_regex || []} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'key-sources',
|
||||
|
||||
+1
-3
@@ -149,9 +149,7 @@ export function AmountDiscountVisualEditor({
|
||||
id: 'amount',
|
||||
header: t('Recharge Amount'),
|
||||
cell: (discount) => (
|
||||
<span className='font-mono text-sm'>
|
||||
${discount.amount}
|
||||
</span>
|
||||
<span className='font-mono text-sm'>${discount.amount}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
+1
-3
@@ -26,9 +26,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
StaticDataTable,
|
||||
} from '@/components/data-table'
|
||||
import { StaticDataTable } from '@/components/data-table'
|
||||
import { safeJsonParseWithValidation } from '../utils/json-parser'
|
||||
import { isArray } from '../utils/json-validators'
|
||||
import {
|
||||
|
||||
+7
-9
@@ -27,9 +27,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {
|
||||
StaticDataTable,
|
||||
} from '@/components/data-table'
|
||||
import { StaticDataTable } from '@/components/data-table'
|
||||
|
||||
export type ConflictItem = {
|
||||
channel: string
|
||||
@@ -86,18 +84,18 @@ export function ConflictConfirmDialog({
|
||||
id: 'current',
|
||||
header: t('Current Billing'),
|
||||
cell: (conflict) => (
|
||||
<pre className='text-sm whitespace-pre-wrap'>
|
||||
{conflict.current}
|
||||
</pre>
|
||||
<pre className='text-sm whitespace-pre-wrap'>
|
||||
{conflict.current}
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'new',
|
||||
header: t('Change To'),
|
||||
cell: (conflict) => (
|
||||
<pre className='text-sm whitespace-pre-wrap'>
|
||||
{conflict.newVal}
|
||||
</pre>
|
||||
<pre className='text-sm whitespace-pre-wrap'>
|
||||
{conflict.newVal}
|
||||
</pre>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -263,11 +263,11 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({
|
||||
id: 'tool',
|
||||
header: t('Tool identifier'),
|
||||
cell: (row) => (
|
||||
<Input
|
||||
value={row.key}
|
||||
placeholder='web_search_preview:gpt-4o*'
|
||||
onChange={(e) => updateRow(row.id, 'key', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
value={row.key}
|
||||
placeholder='web_search_preview:gpt-4o*'
|
||||
onChange={(e) => updateRow(row.id, 'key', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -275,15 +275,15 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({
|
||||
header: t('Price ($/1K calls)'),
|
||||
className: 'w-[200px]',
|
||||
cell: (row) => (
|
||||
<Input
|
||||
type='number'
|
||||
min={0}
|
||||
step={0.5}
|
||||
value={row.price}
|
||||
onChange={(e) =>
|
||||
updateRow(row.id, 'price', Number(e.target.value) || 0)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
type='number'
|
||||
min={0}
|
||||
step={0.5}
|
||||
value={row.price}
|
||||
onChange={(e) =>
|
||||
updateRow(row.id, 'price', Number(e.target.value) || 0)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -292,14 +292,14 @@ export const ToolPriceSettings = memo(function ToolPriceSettings({
|
||||
className: 'w-[80px] text-right',
|
||||
cellClassName: 'text-right',
|
||||
cell: (row) => (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => removeRow(row.id)}
|
||||
aria-label={t('Delete')}
|
||||
>
|
||||
<Trash2 className='text-destructive h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => removeRow(row.id)}
|
||||
aria-label={t('Delete')}
|
||||
>
|
||||
<Trash2 className='text-destructive h-4 w-4' />
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -487,7 +487,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
// Channel update records which fields changed (stable field tokens); render
|
||||
// them with their localized labels for admins.
|
||||
const changedFieldTokens =
|
||||
isManage && props.isAdmin && Array.isArray(other?.op?.params?.changed_fields)
|
||||
isManage &&
|
||||
props.isAdmin &&
|
||||
Array.isArray(other?.op?.params?.changed_fields)
|
||||
? (other.op.params.changed_fields as string[])
|
||||
: []
|
||||
const changedFieldsText = changedFieldTokens
|
||||
|
||||
@@ -44,7 +44,7 @@ export function AffiliateRewardsCard({
|
||||
const { t } = useTranslation()
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className='bg-muted/20 py-0'>
|
||||
<Card data-card-hover='false' className='bg-muted/20 py-0'>
|
||||
<CardContent className='grid gap-4 p-3 sm:p-4 lg:grid-cols-[minmax(220px,1fr)_minmax(220px,0.72fr)_minmax(320px,1.15fr)] lg:items-center'>
|
||||
<div>
|
||||
<Skeleton className='h-5 w-32' />
|
||||
@@ -60,7 +60,7 @@ export function AffiliateRewardsCard({
|
||||
const hasRewards = (user?.aff_quota ?? 0) > 0
|
||||
|
||||
return (
|
||||
<Card className='bg-muted/20 py-0'>
|
||||
<Card data-card-hover='false' className='bg-muted/20 py-0'>
|
||||
<CardContent className='grid gap-3 p-3 sm:gap-4 sm:p-4 lg:grid-cols-[minmax(200px,1fr)_minmax(180px,0.65fr)_minmax(280px,1fr)] lg:items-center'>
|
||||
<div className='flex min-w-0 items-center gap-2.5'>
|
||||
<div className='bg-background flex size-8 shrink-0 items-center justify-center rounded-lg border'>
|
||||
|
||||
@@ -55,7 +55,8 @@ export function CreemProductsSection({
|
||||
{products.map((product) => (
|
||||
<Card
|
||||
key={product.productId}
|
||||
className='hover:border-foreground/50 cursor-pointer transition-all hover:shadow-md'
|
||||
data-card-hover='false'
|
||||
className='cursor-pointer'
|
||||
onClick={() => onProductSelect(product)}
|
||||
>
|
||||
<CardContent className='p-3 text-center sm:p-4'>
|
||||
@@ -63,7 +64,7 @@ export function CreemProductsSection({
|
||||
<div className='text-muted-foreground mb-2 text-sm'>
|
||||
{t('Quota')}: {formatNumber(product.quota)}
|
||||
</div>
|
||||
<div className='text-lg font-semibold text-indigo-600'>
|
||||
<div className='text-primary text-lg font-semibold'>
|
||||
{formatCreemPrice(product.price, product.currency)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ export function BillingHistoryDialog({
|
||||
return (
|
||||
<div
|
||||
key={record.id}
|
||||
className='hover:bg-muted/50 rounded-lg border p-3 transition-colors sm:p-4'
|
||||
className='rounded-lg border p-3 sm:p-4'
|
||||
>
|
||||
{/* Header Row */}
|
||||
<div className='flex items-start justify-between gap-2'>
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export function CreemConfirmDialog({
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground'>{t('Price')}</span>
|
||||
<span className='font-medium text-indigo-600'>
|
||||
<span className='text-primary font-medium'>
|
||||
{formatCreemPrice(product.price, product.currency)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,7 @@ export function RechargeFormCard({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
|
||||
<Skeleton className='h-6 w-32' />
|
||||
<Skeleton className='mt-2 h-4 w-48' />
|
||||
@@ -191,6 +191,7 @@ export function RechargeFormCard({
|
||||
title={t('Add Funds')}
|
||||
description={t('Choose an amount and payment method')}
|
||||
icon={<WalletCards className='h-4 w-4' />}
|
||||
disableHoverEffect
|
||||
action={
|
||||
onOpenBilling ? (
|
||||
<Button
|
||||
@@ -238,7 +239,7 @@ export function RechargeFormCard({
|
||||
key={index}
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'hover:border-foreground flex min-h-16 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[72px] sm:p-4',
|
||||
'flex min-h-16 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[72px] sm:p-4',
|
||||
selectedPreset === preset.value
|
||||
? 'border-foreground bg-foreground/5 dark:border-foreground dark:bg-foreground/10'
|
||||
: 'border-muted'
|
||||
|
||||
@@ -235,7 +235,7 @@ export function SubscriptionPlansCard({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className='gap-0 overflow-hidden py-0'>
|
||||
<Card data-card-hover='false' className='gap-0 overflow-hidden py-0'>
|
||||
<CardHeader className='border-b p-3 !pb-3 sm:p-5 sm:!pb-5'>
|
||||
<Skeleton className='h-6 w-32' />
|
||||
</CardHeader>
|
||||
@@ -261,6 +261,7 @@ export function SubscriptionPlansCard({
|
||||
title={t('Subscription Plans')}
|
||||
description={t('Subscribe to a plan for model access')}
|
||||
icon={<Crown className='h-4 w-4' />}
|
||||
disableHoverEffect
|
||||
contentClassName='space-y-4 sm:space-y-5'
|
||||
>
|
||||
{/* My subscriptions & billing preference */}
|
||||
@@ -539,10 +540,8 @@ export function SubscriptionPlansCard({
|
||||
return (
|
||||
<Card
|
||||
key={plan.id}
|
||||
className={cn(
|
||||
'transition-shadow hover:shadow-md',
|
||||
isPopular && 'border-primary/70 shadow-sm'
|
||||
)}
|
||||
data-card-hover='false'
|
||||
className={cn(isPopular && 'border-primary/70 shadow-sm')}
|
||||
>
|
||||
<CardContent className='flex h-full flex-col p-3.5 sm:p-4'>
|
||||
<div className='mb-2 flex items-start justify-between gap-3'>
|
||||
|
||||
Vendored
+3
-3
@@ -477,19 +477,19 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
/* Micro-interactions — Vercel-style subtle hover/active feedback */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-slot='card'] {
|
||||
[data-slot='card']:not([data-card-hover='false']) {
|
||||
transition:
|
||||
transform 150ms ease,
|
||||
box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
[data-slot='card']:hover {
|
||||
[data-slot='card']:not([data-card-hover='false']):hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.06);
|
||||
}
|
||||
|
||||
.dark [data-slot='card']:hover {
|
||||
.dark [data-slot='card']:not([data-card-hover='false']):hover {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
-3
@@ -25,8 +25,8 @@ declare module '@tanstack/react-table' {
|
||||
className?: string
|
||||
pinned?: 'left' | 'right'
|
||||
// Mobile card list layout hints (used by MobileCardList)
|
||||
mobileTitle?: boolean // card title area (left, larger text)
|
||||
mobileBadge?: boolean // status badge alongside title (right)
|
||||
mobileHidden?: boolean // hide this column on mobile entirely
|
||||
mobileTitle?: boolean // card title area (left, larger text)
|
||||
mobileBadge?: boolean // status badge alongside title (right)
|
||||
mobileHidden?: boolean // hide this column on mobile entirely
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user