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