Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
eb863116
authored
Jun 16, 2026
by
t0ng7u
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
style(ui): align wallet and profile interactions
parent
b36888ba
Hide whitespace changes
Inline
Side-by-side
Showing
39 changed files
with
338 additions
and
311 deletions
+338
-311
web/default/src/components/data-table/core/data-table-header.tsx
+5
-1
web/default/src/components/data-table/layout/data-table-page.tsx
+2
-1
web/default/src/components/data-table/layout/mobile-card-list.tsx
+5
-2
web/default/src/components/data-table/static/static-data-table-classnames.ts
+2
-1
web/default/src/components/provider-badge.tsx
+2
-8
web/default/src/components/ui/table.tsx
+1
-1
web/default/src/components/ui/titled-card.tsx
+6
-1
web/default/src/features/channels/components/channels-columns.tsx
+11
-4
web/default/src/features/dashboard/components/overview/overview-dashboard.tsx
+4
-2
web/default/src/features/models/components/data-table-row-actions.tsx
+66
-66
web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx
+3
-5
web/default/src/features/pricing/components/model-card.tsx
+1
-3
web/default/src/features/profile/components/checkin-calendar-card.tsx
+8
-9
web/default/src/features/profile/components/language-preferences-card.tsx
+1
-0
web/default/src/features/profile/components/passkey-card.tsx
+3
-3
web/default/src/features/profile/components/profile-header.tsx
+19
-12
web/default/src/features/profile/components/profile-security-card.tsx
+4
-5
web/default/src/features/profile/components/profile-settings-card.tsx
+2
-1
web/default/src/features/profile/components/sidebar-modules-card.tsx
+2
-2
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
+1
-1
web/default/src/features/profile/components/tabs/notification-tab.tsx
+38
-29
web/default/src/features/profile/components/two-fa-card.tsx
+2
-2
web/default/src/features/redemption-codes/components/data-table-row-actions.tsx
+56
-56
web/default/src/features/subscriptions/components/data-table-row-actions.tsx
+36
-36
web/default/src/features/system-settings/content/uptime-kuma-section.tsx
+1
-2
web/default/src/features/system-settings/general/channel-affinity/index.tsx
+3
-1
web/default/src/features/system-settings/integrations/amount-discount-visual-editor.tsx
+1
-3
web/default/src/features/system-settings/integrations/payment-methods-visual-editor.tsx
+1
-3
web/default/src/features/system-settings/models/conflict-confirm-dialog.tsx
+7
-9
web/default/src/features/system-settings/models/tool-price-settings.tsx
+22
-22
web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
+3
-1
web/default/src/features/wallet/components/affiliate-rewards-card.tsx
+2
-2
web/default/src/features/wallet/components/creem-products-section.tsx
+3
-2
web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx
+1
-1
web/default/src/features/wallet/components/dialogs/creem-confirm-dialog.tsx
+1
-1
web/default/src/features/wallet/components/recharge-form-card.tsx
+3
-2
web/default/src/features/wallet/components/subscription-plans-card.tsx
+4
-5
web/default/src/styles/index.css
+3
-3
web/default/src/tanstack-table.d.ts
+3
-3
No files found.
web/default/src/components/data-table/core/data-table-header.tsx
View file @
eb863116
...
...
@@ -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'
...
...
web/default/src/components/data-table/layout/data-table-page.tsx
View file @
eb863116
...
...
@@ -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
}
...
...
web/default/src/components/data-table/layout/mobile-card-list.tsx
View file @
eb863116
...
...
@@ -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
>
...
...
web/default/src/components/data-table/static/static-data-table-classnames.ts
View file @
eb863116
...
...
@@ -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
:
...
...
web/default/src/components/provider-badge.tsx
View file @
eb863116
...
...
@@ -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
>
...
...
web/default/src/components/ui/table.tsx
View file @
eb863116
...
...
@@ -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
}
...
...
web/default/src/components/ui/titled-card.tsx
View file @
eb863116
...
...
@@ -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
)
}
>
...
...
web/default/src/features/channels/components/channels-columns.tsx
View file @
eb863116
...
...
@@ -622,7 +622,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
:
undefined
return
(
<
div
className=
'flex m
in-w-0 max-w-full
items-center gap-2 overflow-hidden'
>
<
div
className=
'flex m
ax-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=
'm
in-w-0 max-w-full
overflow-hidden'
/>
<
div
className=
'm
ax-w-full min-w-0
overflow-hidden'
/>
}
>
<
ProviderBadge
...
...
@@ -649,7 +649,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
label=
{
typeName
}
copyable=
{
false
}
showDot=
{
false
}
className=
'm
in-w-0 max-w-full
overflow-hidden'
className=
'm
ax-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
,
...
...
web/default/src/features/dashboard/components/overview/overview-dashboard.tsx
View file @
eb863116
...
...
@@ -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
])
...
...
web/default/src/features/models/components/data-table-row-actions.tsx
View file @
eb863116
...
...
@@ -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
>
<
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
/>
<
DropdownMenuSeparator
/>
{
/* Enable/Disable */
}
<
DropdownMenuItem
onClick=
{
handleToggleStatus
}
>
{
isEnabled
?
(
<>
{
t
(
'Disable'
)
}
<
DropdownMenuShortcut
>
<
PowerOff
size=
{
16
}
/>
</
DropdownMenuShortcut
>
</>
)
:
(
<>
{
t
(
'Enable'
)
}
<
DropdownMenuShortcut
>
<
Power
size=
{
16
}
/>
</
DropdownMenuShortcut
>
</>
)
}
</
DropdownMenuItem
>
{
/* Enable/Disable */
}
<
DropdownMenuItem
onClick=
{
handleToggleStatus
}
>
{
isEnabled
?
(
<>
{
t
(
'Disable'
)
}
<
DropdownMenuShortcut
>
<
PowerOff
size=
{
16
}
/>
</
DropdownMenuShortcut
>
</>
)
:
(
<>
{
t
(
'Enable'
)
}
<
DropdownMenuShortcut
>
<
Power
size=
{
16
}
/>
</
DropdownMenuShortcut
>
</>
)
}
</
DropdownMenuItem
>
<
DropdownMenuSeparator
/>
<
DropdownMenuSeparator
/>
{
/* Delete */
}
<
DropdownMenuItem
onSelect=
{
(
e
)
=>
{
e
.
preventDefault
()
setDeleteConfirmOpen
(
true
)
}
}
className=
'text-destructive focus:text-destructive'
>
{
t
(
'Delete'
)
}
<
DropdownMenuShortcut
>
<
Trash2
size=
{
16
}
/>
</
DropdownMenuShortcut
>
</
DropdownMenuItem
>
</
DropdownMenuContent
>
{
/* 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
>
<
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
>
)
}
web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx
View file @
eb863116
...
...
@@ -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'
>
...
...
web/default/src/features/pricing/components/model-card.tsx
View file @
eb863116
...
...
@@ -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'
&&
...
...
web/default/src/features/profile/components/checkin-calendar-card.tsx
View file @
eb863116
...
...
@@ -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'
>
<
B
utton
<
b
utton
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
>
</
B
utton
>
</
b
utton
>
<
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
>
)
}
web/default/src/features/profile/components/language-preferences-card.tsx
View file @
eb863116
...
...
@@ -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'
>
...
...
web/default/src/features/profile/components/passkey-card.tsx
View file @
eb863116
...
...
@@ -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
()
...
...
web/default/src/features/profile/components/profile-header.tsx
View file @
eb863116
...
...
@@ -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
{
get
UserInitials
,
get
DisplayName
}
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
>
)
}
web/default/src/features/profile/components/profile-security-card.tsx
View file @
eb863116
...
...
@@ -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
...
...
web/default/src/features/profile/components/profile-settings-card.tsx
View file @
eb863116
...
...
@@ -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'
>
...
...
web/default/src/features/profile/components/sidebar-modules-card.tsx
View file @
eb863116
...
...
@@ -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'
}`
}
>
...
...
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
View file @
eb863116
...
...
@@ -350,7 +350,7 @@ export function AccountBindingsTab({
<
Button
variant=
'ghost'
size=
'sm'
className=
'text-destructive h
over: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'
/>
...
...
web/default/src/features/profile/components/tabs/notification-tab.tsx
View file @
eb863116
...
...
@@ -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
>
)
})
}
</
Radio
Group
>
</
Toggle
Group
>
</
div
>
{
/* Warning Threshold */
}
...
...
@@ -178,7 +187,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</
div
>
{
/* Email Settings */
}
{
settings
.
notify_t
ype
===
'email'
&&
(
{
notifyT
ype
===
'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_t
ype
===
'webhook'
&&
(
{
notifyT
ype
===
'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_t
ype
===
'bark'
&&
(
{
notifyT
ype
===
'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_t
ype
===
'gotify'
&&
(
{
notifyT
ype
===
'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
>
...
...
web/default/src/features/profile/components/two-fa-card.tsx
View file @
eb863116
...
...
@@ -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'
)
}
...
...
web/default/src/features/redemption-codes/components/data-table-row-actions.tsx
View file @
eb863116
...
...
@@ -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
>
)
}
web/default/src/features/subscriptions/components/data-table-row-actions.tsx
View file @
eb863116
...
...
@@ -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
>
)
}
web/default/src/features/system-settings/content/uptime-kuma-section.tsx
View file @
eb863116
...
...
@@ -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
,
},
{
...
...
web/default/src/features/system-settings/general/channel-affinity/index.tsx
View file @
eb863116
...
...
@@ -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'
,
...
...
web/default/src/features/system-settings/integrations/amount-discount-visual-editor.tsx
View file @
eb863116
...
...
@@ -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
>
),
},
{
...
...
web/default/src/features/system-settings/integrations/payment-methods-visual-editor.tsx
View file @
eb863116
...
...
@@ -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
{
...
...
web/default/src/features/system-settings/models/conflict-confirm-dialog.tsx
View file @
eb863116
...
...
@@ -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
>
),
},
]
}
...
...
web/default/src/features/system-settings/models/tool-price-settings.tsx
View file @
eb863116
...
...
@@ -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
>
),
},
]
}
...
...
web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
View file @
eb863116
...
...
@@ -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
...
...
web/default/src/features/wallet/components/affiliate-rewards-card.tsx
View file @
eb863116
...
...
@@ -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'
>
...
...
web/default/src/features/wallet/components/creem-products-section.tsx
View file @
eb863116
...
...
@@ -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
>
...
...
web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx
View file @
eb863116
...
...
@@ -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'
>
...
...
web/default/src/features/wallet/components/dialogs/creem-confirm-dialog.tsx
View file @
eb863116
...
...
@@ -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
>
...
...
web/default/src/features/wallet/components/recharge-form-card.tsx
View file @
eb863116
...
...
@@ -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'
...
...
web/default/src/features/wallet/components/subscription-plans-card.tsx
View file @
eb863116
...
...
@@ -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'
>
...
...
web/default/src/styles/index.css
View file @
eb863116
...
...
@@ -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
);
}
}
...
...
web/default/src/tanstack-table.d.ts
View file @
eb863116
...
...
@@ -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
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment