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
Unverified
Commit
f9825448
authored
Apr 29, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ui): refine default frontend layouts
parent
43841070
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
28 changed files
with
668 additions
and
345 deletions
+668
-345
web/default/src/components/layout/lib/url-utils.ts
+6
-2
web/default/src/components/layout/types.ts
+2
-0
web/default/src/features/dashboard/index.tsx
+37
-2
web/default/src/features/models/index.tsx
+59
-21
web/default/src/features/profile/components/passkey-card.tsx
+16
-10
web/default/src/features/profile/components/profile-header.tsx
+92
-38
web/default/src/features/profile/components/profile-security-card.tsx
+27
-14
web/default/src/features/profile/components/profile-settings-card.tsx
+35
-16
web/default/src/features/profile/components/sidebar-modules-card.tsx
+25
-16
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
+7
-7
web/default/src/features/profile/components/tabs/notification-tab.tsx
+7
-4
web/default/src/features/profile/components/two-fa-card.tsx
+19
-10
web/default/src/features/profile/index.tsx
+12
-12
web/default/src/features/usage-logs/index.tsx
+80
-20
web/default/src/features/wallet/components/affiliate-rewards-card.tsx
+39
-23
web/default/src/features/wallet/components/recharge-form-card.tsx
+46
-30
web/default/src/features/wallet/components/subscription-plans-card.tsx
+0
-0
web/default/src/features/wallet/components/wallet-stats-card.tsx
+55
-37
web/default/src/features/wallet/index.tsx
+48
-46
web/default/src/hooks/use-sidebar-config.ts
+6
-1
web/default/src/hooks/use-sidebar-data.ts
+17
-9
web/default/src/i18n/locales/en.json
+1
-0
web/default/src/i18n/locales/fr.json
+1
-0
web/default/src/i18n/locales/ja.json
+1
-0
web/default/src/i18n/locales/ru.json
+1
-0
web/default/src/i18n/locales/vi.json
+1
-0
web/default/src/i18n/locales/zh.json
+1
-0
web/default/src/styles/theme.css
+27
-27
No files found.
web/default/src/components/layout/lib/url-utils.ts
View file @
f9825448
...
...
@@ -40,11 +40,16 @@ export function checkIsActive(
item
:
NavItem
,
mainNav
=
false
):
boolean
{
const
hrefWithoutQuery
=
href
.
split
(
'?'
)[
0
]
if
(
item
.
activeUrls
?.
some
((
url
)
=>
urlToString
(
url
)
===
hrefWithoutQuery
))
{
return
true
}
// For collapsible items (NavCollapsible), check sub-items first
if
(
'items'
in
item
&&
item
.
items
)
{
const
collapsibleItem
=
item
as
NavCollapsible
const
items
=
collapsibleItem
.
items
const
hrefWithoutQuery
=
href
.
split
(
'?'
)[
0
]
// Check if any sub-item matches
if
(
...
...
@@ -76,7 +81,6 @@ export function checkIsActive(
// Exact match
if
(
href
===
itemUrl
)
return
true
const
hrefWithoutQuery
=
href
.
split
(
'?'
)[
0
]
const
itemUrlWithoutQuery
=
itemUrl
.
split
(
'?'
)[
0
]
const
itemUrlHasQuery
=
itemUrl
.
includes
(
'?'
)
...
...
web/default/src/components/layout/types.ts
View file @
f9825448
...
...
@@ -18,6 +18,8 @@ type BaseNavItem = {
title
:
string
badge
?:
string
icon
?:
React
.
ElementType
activeUrls
?:
(
LinkProps
[
'to'
]
|
(
string
&
{}))[]
configUrls
?:
(
LinkProps
[
'to'
]
|
(
string
&
{}))[]
}
/**
...
...
web/default/src/features/dashboard/index.tsx
View file @
f9825448
import
{
useState
,
useCallback
,
lazy
,
Suspense
}
from
'react'
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
useState
,
useCallback
,
useMemo
,
lazy
,
Suspense
}
from
'react'
import
{
getRouteApi
,
useNavigate
}
from
'@tanstack/react-router'
import
{
useTranslation
}
from
'react-i18next'
import
{
useAuthStore
}
from
'@/stores/auth-store'
import
{
ROLE
}
from
'@/lib/roles'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
Tabs
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
import
{
SectionPageLayout
}
from
'@/components/layout'
import
{
CardStaggerContainer
,
...
...
@@ -18,6 +21,7 @@ import { DEFAULT_TIME_GRANULARITY } from './constants'
import
{
type
DashboardSectionId
,
DASHBOARD_DEFAULT_SECTION
,
DASHBOARD_SECTION_IDS
,
}
from
'./section-registry'
import
{
type
DashboardFilters
,
type
QuotaDataItem
}
from
'./types'
...
...
@@ -97,7 +101,9 @@ const SECTION_META: Record<
export
function
Dashboard
()
{
const
{
t
}
=
useTranslation
()
const
navigate
=
useNavigate
()
const
params
=
route
.
useParams
()
const
userRole
=
useAuthStore
((
state
)
=>
state
.
auth
.
user
?.
role
)
const
activeSection
=
(
params
.
section
??
DASHBOARD_DEFAULT_SECTION
)
as
DashboardSectionId
...
...
@@ -122,6 +128,24 @@ export function Dashboard() {
)
const
meta
=
SECTION_META
[
activeSection
]
??
SECTION_META
.
overview
const
isAdmin
=
Boolean
(
userRole
&&
userRole
>=
ROLE
.
ADMIN
)
const
visibleSections
=
useMemo
(
()
=>
DASHBOARD_SECTION_IDS
.
filter
(
(
section
)
=>
section
!==
'overview'
&&
(
section
!==
'users'
||
isAdmin
)
),
[
isAdmin
]
)
const
handleSectionChange
=
useCallback
(
(
section
:
string
)
=>
{
void
navigate
({
to
:
'/dashboard/$section'
,
params
:
{
section
:
section
as
DashboardSectionId
},
})
},
[
navigate
]
)
const
showSectionTabs
=
activeSection
!==
'overview'
&&
visibleSections
.
length
>
1
return
(
<
SectionPageLayout
>
...
...
@@ -139,6 +163,17 @@ export function Dashboard() {
)
}
<
SectionPageLayout
.
Content
>
<
div
className=
'space-y-4'
>
{
showSectionTabs
&&
(
<
Tabs
value=
{
activeSection
}
onValueChange=
{
handleSectionChange
}
>
<
TabsList
className=
'h-auto max-w-full flex-wrap justify-start'
>
{
visibleSections
.
map
((
section
)
=>
(
<
TabsTrigger
key=
{
section
}
value=
{
section
}
>
{
t
(
SECTION_META
[
section
].
titleKey
)
}
</
TabsTrigger
>
))
}
</
TabsList
>
</
Tabs
>
)
}
{
activeSection
===
'overview'
&&
(
<>
<
SummaryCards
/>
...
...
web/default/src/features/models/index.tsx
View file @
f9825448
import
{
useEffect
,
useState
}
from
'react'
import
{
use
Callback
,
use
Effect
,
useState
}
from
'react'
import
{
useQueryClient
}
from
'@tanstack/react-query'
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
getRouteApi
,
useNavigate
}
from
'@tanstack/react-router'
import
{
Plus
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
Button
}
from
'@/components/ui/button'
import
{
Tabs
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
import
{
SectionPageLayout
}
from
'@/components/layout'
import
{
listDeployments
}
from
'./api'
import
{
DeploymentAccessGuard
}
from
'./components/deployment-access-guard'
...
...
@@ -18,12 +19,28 @@ import { deploymentsQueryKeys } from './lib'
import
{
type
ModelsSectionId
,
MODELS_DEFAULT_SECTION
,
MODELS_SECTION_IDS
,
}
from
'./section-registry'
const
route
=
getRouteApi
(
'/_authenticated/models/$section'
)
const
SECTION_META
:
Record
<
ModelsSectionId
,
{
titleKey
:
string
;
descriptionKey
:
string
}
>
=
{
metadata
:
{
titleKey
:
'Metadata'
,
descriptionKey
:
'Manage model metadata and configuration'
,
},
deployments
:
{
titleKey
:
'Deployments'
,
descriptionKey
:
'Manage model deployments'
,
},
}
function
ModelsContent
()
{
const
{
t
}
=
useTranslation
()
const
navigate
=
useNavigate
()
const
queryClient
=
useQueryClient
()
const
{
tabCategory
,
setTabCategory
}
=
useModels
()
const
params
=
route
.
useParams
()
...
...
@@ -75,16 +92,26 @@ function ModelsContent() {
}
},
[
activeSection
,
isIoNetEnabled
,
loadingPhase
,
queryClient
])
const
handleSectionChange
=
useCallback
(
(
section
:
string
)
=>
{
void
navigate
({
to
:
'/models/$section'
,
params
:
{
section
:
section
as
ModelsSectionId
},
})
},
[
navigate
]
)
const
meta
=
SECTION_META
[
activeSection
]
??
SECTION_META
.
metadata
return
(
<>
<
SectionPageLayout
>
<
SectionPageLayout
.
Title
>
{
activeSection
===
'metadata'
?
t
(
'Metadata'
)
:
t
(
'Deployments'
)
}
{
t
(
meta
.
titleKey
)
}
</
SectionPageLayout
.
Title
>
<
SectionPageLayout
.
Description
>
{
activeSection
===
'metadata'
?
t
(
'Manage model metadata and configuration'
)
:
t
(
'Manage model deployments'
)
}
{
t
(
meta
.
descriptionKey
)
}
</
SectionPageLayout
.
Description
>
<
SectionPageLayout
.
Actions
>
{
activeSection
===
'metadata'
?
(
...
...
@@ -97,21 +124,32 @@ function ModelsContent() {
)
}
</
SectionPageLayout
.
Actions
>
<
SectionPageLayout
.
Content
>
{
activeSection
===
'metadata'
?
(
<
ModelsTable
/>
)
:
(
<
DeploymentAccessGuard
loading=
{
deploymentLoading
}
loadingPhase=
{
loadingPhase
}
isEnabled=
{
isIoNetEnabled
}
connectionLoading=
{
connectionLoading
}
connectionOk=
{
connectionOk
}
connectionError=
{
connectionError
}
onRetry=
{
testConnection
}
>
<
DeploymentsTable
/>
</
DeploymentAccessGuard
>
)
}
<
div
className=
'space-y-4'
>
<
Tabs
value=
{
activeSection
}
onValueChange=
{
handleSectionChange
}
>
<
TabsList
className=
'h-auto max-w-full flex-wrap justify-start'
>
{
MODELS_SECTION_IDS
.
map
((
section
)
=>
(
<
TabsTrigger
key=
{
section
}
value=
{
section
}
>
{
t
(
SECTION_META
[
section
].
titleKey
)
}
</
TabsTrigger
>
))
}
</
TabsList
>
</
Tabs
>
{
activeSection
===
'metadata'
?
(
<
ModelsTable
/>
)
:
(
<
DeploymentAccessGuard
loading=
{
deploymentLoading
}
loadingPhase=
{
loadingPhase
}
isEnabled=
{
isIoNetEnabled
}
connectionLoading=
{
connectionLoading
}
connectionOk=
{
connectionOk
}
connectionError=
{
connectionError
}
onRetry=
{
testConnection
}
>
<
DeploymentsTable
/>
</
DeploymentAccessGuard
>
)
}
</
div
>
</
SectionPageLayout
.
Content
>
</
SectionPageLayout
>
...
...
web/default/src/features/profile/components/passkey-card.tsx
View file @
f9825448
...
...
@@ -15,7 +15,13 @@ import {
AlertDialogTrigger
,
}
from
'@/components/ui/alert-dialog'
import
{
Button
}
from
'@/components/ui/button'
import
{
Card
,
CardContent
,
CardHeader
}
from
'@/components/ui/card'
import
{
Card
,
CardContent
,
CardDescription
,
CardHeader
,
CardTitle
,
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
StatusBadge
}
from
'@/components/status-badge'
import
{
usePasskeyManagement
}
from
'@/features/auth/passkey'
...
...
@@ -163,7 +169,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
if
(
pageLoading
||
loading
)
{
return
(
<
Card
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
>
<
Skeleton
className=
'h-6 w-48'
/>
<
Skeleton
className=
'mt-2 h-4 w-64'
/>
...
...
@@ -185,18 +191,18 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
return
(
<>
<
Card
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
>
<
h3
className=
'text-xl font-semibold
tracking-tight'
>
<
CardTitle
className=
'text-xl
tracking-tight'
>
{
t
(
'Passkey Login'
)
}
</
h3
>
<
p
className=
'text-muted-foreground mt-2 text-sm'
>
</
CardTitle
>
<
CardDescription
>
{
t
(
'Use Passkey to sign in without entering your password.'
)
}
</
p
>
</
CardDescription
>
</
CardHeader
>
<
CardContent
className=
'space-y-6'
>
<
div
className=
'flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between'
>
<
div
className=
'flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between
xl:flex-col xl:items-stretch 2xl:flex-row 2xl:items-center
'
>
<
div
className=
'flex items-start gap-3'
>
<
div
className=
'bg-muted rounded-md p-2'
>
<
KeyRound
className=
'h-5 w-5'
/>
...
...
@@ -239,7 +245,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
{
!
enabled
?
(
<
Button
className=
'w-full sm:w-auto'
className=
'w-full sm:w-auto
xl:w-full 2xl:w-auto
'
onClick=
{
handleRegister
}
disabled=
{
!
supported
||
registering
}
>
...
...
@@ -253,7 +259,7 @@ export function PasskeyCard({ loading: pageLoading }: PasskeyCardProps) {
<
AlertDialogTrigger
asChild
>
<
Button
variant=
'outline'
className=
'w-full sm:w-auto'
className=
'w-full sm:w-auto
xl:w-full 2xl:w-auto
'
disabled=
{
removing
}
>
{
t
(
'Remove Passkey'
)
}
...
...
web/default/src/features/profile/components/profile-header.tsx
View file @
f9825448
import
{
useTranslation
}
from
'react-i18next'
import
{
formatCompactNumber
,
formatQuota
}
from
'@/lib/format'
import
{
getRoleLabel
}
from
'@/lib/roles'
import
{
Avatar
,
AvatarFallback
}
from
'@/components/ui/avatar'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
...
...
@@ -15,20 +17,34 @@ interface ProfileHeaderProps {
}
export
function
ProfileHeader
({
profile
,
loading
}:
ProfileHeaderProps
)
{
const
{
t
}
=
useTranslation
()
if
(
loading
)
{
return
(
<
div
className=
'space-y-6'
>
<
div
className=
'flex flex-col items-center gap-4 text-center lg:flex-row lg:items-center lg:gap-6 lg:text-left'
>
<
Skeleton
className=
'h-20 w-20 rounded-full'
/>
<
div
className=
'flex-1 space-y-3 lg:space-y-2'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-center lg:justify-start'
>
<
Skeleton
className=
'h-8 w-48'
/>
<
Skeleton
className=
'h-5 w-16'
/>
<
div
className=
'bg-card overflow-hidden rounded-2xl border'
>
<
div
className=
'p-5 sm:p-6'
>
<
div
className=
'flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
Skeleton
className=
'h-20 w-20 rounded-2xl'
/>
<
div
className=
'space-y-3'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
Skeleton
className=
'h-8 w-48'
/>
<
Skeleton
className=
'h-5 w-16'
/>
</
div
>
<
div
className=
'flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'
>
<
Skeleton
className=
'h-4 w-24'
/>
<
Skeleton
className=
'h-4 w-40'
/>
<
Skeleton
className=
'h-4 w-20'
/>
</
div
>
</
div
>
</
div
>
<
div
className=
'flex flex-col items-center gap-1 sm:flex-row sm:justify-center sm:gap-4 lg:justify-start'
>
<
Skeleton
className=
'h-4 w-24'
/>
<
Skeleton
className=
'h-4 w-40'
/>
<
Skeleton
className=
'h-4 w-20'
/>
<
div
className=
'grid gap-3 sm:grid-cols-3 lg:w-[480px]'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
i
}
className=
'rounded-xl border p-4'
>
<
Skeleton
className=
'mb-3 h-3 w-20'
/>
<
Skeleton
className=
'h-7 w-24'
/>
</
div
>
))
}
</
div
>
</
div
>
</
div
>
...
...
@@ -41,38 +57,76 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
const
displayName
=
getDisplayName
(
profile
)
const
initials
=
getUserInitials
(
profile
)
const
roleLabel
=
getRoleLabel
(
profile
.
role
)
const
stats
=
[
{
label
:
t
(
'Current Balance'
),
value
:
formatQuota
(
profile
.
quota
),
},
{
label
:
t
(
'Total Usage'
),
value
:
formatQuota
(
profile
.
used_quota
),
},
{
label
:
t
(
'API Requests'
),
value
:
formatCompactNumber
(
profile
.
request_count
),
},
]
return
(
<
div
className=
'space-y-6'
>
<
div
className=
'flex flex-col items-center gap-4 text-center lg:flex-row lg:items-center lg:gap-6 lg:text-left'
>
<
Avatar
className=
'h-20 w-20 text-xl'
>
<
AvatarFallback
className=
'bg-primary/10 text-primary'
>
{
initials
}
</
AvatarFallback
>
</
Avatar
>
<
div
className=
'bg-card relative overflow-hidden rounded-2xl border'
>
<
div
className=
'relative p-5 sm:p-6'
>
<
div
className=
'flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between'
>
<
div
className=
'flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'
>
<
Avatar
className=
'ring-background h-20 w-20 rounded-2xl text-xl ring-4'
>
<
AvatarFallback
className=
'bg-primary/10 text-primary rounded-2xl'
>
{
initials
}
</
AvatarFallback
>
</
Avatar
>
<
div
className=
'min-w-0 flex-1 space-y-3'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-start'
>
<
h1
className=
'text-2xl font-semibold tracking-tight sm:text-3xl'
>
{
displayName
}
</
h1
>
<
StatusBadge
label=
{
roleLabel
}
variant=
'neutral'
copyable=
{
false
}
/>
</
div
>
<
div
className=
'flex-1 space-y-3 lg:space-y-2'
>
<
div
className=
'flex flex-col items-center gap-2 sm:flex-row sm:justify-center lg:justify-start'
>
<
h1
className=
'text-3xl font-semibold tracking-tight'
>
{
displayName
}
</
h1
>
<
StatusBadge
label=
{
roleLabel
}
variant=
'neutral'
copyable=
{
false
}
/>
<
div
className=
'text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-start sm:gap-4'
>
<
span
>
@
{
profile
.
username
}
</
span
>
{
profile
.
email
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
email
}
</
span
>
</>
)
}
{
profile
.
group
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
group
}
</
span
>
</>
)
}
</
div
>
</
div
>
</
div
>
<
div
className=
'
text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-center sm:gap-4 lg:justify-start
'
>
<
span
>
@
{
profile
.
username
}
</
span
>
{
profile
.
email
&&
(
<>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
email
}
</
span
>
</
>
)
}
{
profile
.
group
&&
(
<
>
<
span
className=
'hidden sm:inline'
>
•
</
span
>
<
span
>
{
profile
.
group
}
</
span
>
</>
)
}
<
div
className=
'
grid gap-3 sm:grid-cols-3 lg:w-[480px]
'
>
{
stats
.
map
((
item
)
=>
(
<
div
key=
{
item
.
label
}
className=
'bg-background/70 rounded-xl border p-4 backdrop-blur'
>
<
p
className=
'text-muted-foreground text-xs font-medium'
>
{
item
.
label
}
</
p
>
<
p
className=
'mt-2 truncate text-xl font-semibold tracking-tight'
>
{
item
.
value
}
<
/
p
>
</
div
>
)
)
}
</
div
>
</
div
>
</
div
>
...
...
web/default/src/features/profile/components/profile-security-card.tsx
View file @
f9825448
import
{
Shield
,
Key
,
Trash2
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useDialogs
}
from
'@/hooks/use-dialog'
import
{
Card
,
CardContent
,
CardHeader
}
from
'@/components/ui/card'
import
{
Card
,
CardContent
,
CardDescription
,
CardHeader
,
CardTitle
,
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
type
{
UserProfile
}
from
'../types'
import
{
AccessTokenDialog
}
from
'./dialogs/access-token-dialog'
...
...
@@ -28,12 +34,12 @@ export function ProfileSecurityCard({
if
(
loading
)
{
return
(
<
Card
>
<
CardHeader
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
Skeleton
className=
'h-6 w-32'
/>
<
Skeleton
className=
'mt-2 h-4 w-48'
/>
</
CardHeader
>
<
CardContent
className=
'space-y-3'
>
<
CardContent
className=
'space-y-3
pt-6
'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
Skeleton
key=
{
i
}
className=
'h-16 w-full'
/>
))
}
...
...
@@ -70,18 +76,25 @@ export function ProfileSecurityCard({
return
(
<>
<
Card
>
<
CardHeader
>
<
h3
className=
'text-xl font-semibold tracking-tight'
>
{
t
(
'Security'
)
}
</
h3
>
<
p
className=
'text-muted-foreground mt-2 text-sm'
>
{
t
(
'Manage your security settings and account access'
)
}
</
p
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
div
className=
'flex items-center gap-3'
>
<
div
className=
'bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'
>
<
Shield
className=
'h-4 w-4'
/>
</
div
>
<
div
className=
'min-w-0'
>
<
CardTitle
className=
'text-xl tracking-tight'
>
{
t
(
'Security'
)
}
</
CardTitle
>
<
CardDescription
>
{
t
(
'Manage your security settings and account access'
)
}
</
CardDescription
>
</
div
>
</
div
>
</
CardHeader
>
<
CardContent
>
<
div
className=
'grid grid-cols-1 gap-3
sm
:grid-cols-3'
>
<
CardContent
className=
'pt-6'
>
<
div
className=
'grid grid-cols-1 gap-3
md
:grid-cols-3'
>
{
securityActions
.
map
((
item
)
=>
(
<
button
key=
{
item
.
title
}
...
...
web/default/src/features/profile/components/profile-settings-card.tsx
View file @
f9825448
import
{
useState
}
from
'react'
import
{
Link2
,
Settings
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
Card
,
CardContent
,
CardHeader
}
from
'@/components/ui/card'
import
{
Card
,
CardContent
,
CardDescription
,
CardHeader
,
CardTitle
,
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
Tabs
,
TabsContent
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
import
type
{
UserProfile
}
from
'../types'
...
...
@@ -28,12 +34,12 @@ export function ProfileSettingsCard({
if
(
loading
)
{
return
(
<
Card
>
<
CardHeader
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
Skeleton
className=
'h-6 w-32'
/>
<
Skeleton
className=
'mt-2 h-4 w-48'
/>
</
CardHeader
>
<
CardContent
className=
'space-y-4'
>
<
CardContent
className=
'space-y-4
pt-6
'
>
<
Skeleton
className=
'h-10 w-full'
/>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
Skeleton
key=
{
i
}
className=
'h-20 w-full'
/>
...
...
@@ -44,25 +50,38 @@ export function ProfileSettingsCard({
}
return
(
<
Card
>
<
CardHeader
>
<
h3
className=
'text-xl font-semibold tracking-tight'
>
{
t
(
'Settings'
)
}
</
h3
>
<
p
className=
'text-muted-foreground mt-2 text-sm'
>
{
t
(
'Configure your account preferences and integrations'
)
}
</
p
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
div
className=
'flex items-center gap-3'
>
<
div
className=
'bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'
>
<
Settings
className=
'h-4 w-4'
/>
</
div
>
<
div
className=
'min-w-0'
>
<
CardTitle
className=
'text-xl tracking-tight'
>
{
t
(
'Settings'
)
}
</
CardTitle
>
<
CardDescription
>
{
t
(
'Configure your account preferences and integrations'
)
}
</
CardDescription
>
</
div
>
</
div
>
</
CardHeader
>
<
CardContent
>
<
CardContent
className=
'pt-6'
>
<
Tabs
value=
{
activeTab
}
onValueChange=
{
setActiveTab
}
>
<
TabsList
className=
'grid w-full grid-cols-2'
>
<
TabsTrigger
value=
'bindings'
className=
'gap-2'
>
<
TabsList
className=
'grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'
>
<
TabsTrigger
value=
'bindings'
className=
'h-auto gap-2 rounded-lg px-3 py-2.5'
>
<
Link2
className=
'h-4 w-4'
/>
<
span
className=
'hidden sm:inline'
>
{
t
(
'Account Bindings'
)
}
</
span
>
<
span
className=
'sm:hidden'
>
{
t
(
'Bindings'
)
}
</
span
>
</
TabsTrigger
>
<
TabsTrigger
value=
'settings'
className=
'gap-2'
>
<
TabsTrigger
value=
'settings'
className=
'h-auto gap-2 rounded-lg px-3 py-2.5'
>
<
Settings
className=
'h-4 w-4'
/>
<
span
className=
'hidden sm:inline'
>
{
t
(
'Settings & Preferences'
)
}
...
...
web/default/src/features/profile/components/sidebar-modules-card.tsx
View file @
f9825448
...
...
@@ -182,23 +182,32 @@ export function SidebarModulesCard() {
}
return
(
<
Card
>
<
CardHeader
>
<
CardTitle
className=
'flex items-center gap-2'
>
<
LayoutDashboard
className=
'h-4 w-4'
/>
{
t
(
'Sidebar Personal Settings'
)
}
</
CardTitle
>
<
CardDescription
>
{
t
(
'Customize sidebar display content'
)
}
</
CardDescription
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
div
className=
'flex items-center gap-3'
>
<
div
className=
'bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'
>
<
LayoutDashboard
className=
'h-4 w-4'
/>
</
div
>
<
div
className=
'min-w-0'
>
<
CardTitle
className=
'text-xl tracking-tight'
>
{
t
(
'Sidebar Personal Settings'
)
}
</
CardTitle
>
<
CardDescription
>
{
t
(
'Customize sidebar display content'
)
}
</
CardDescription
>
</
div
>
</
div
>
</
CardHeader
>
<
CardContent
className=
'space-y-6'
>
<
CardContent
className=
'space-y-
5 pt-
6'
>
{
sectionDefs
.
map
((
section
)
=>
{
const
sectionEnabled
=
config
[
section
.
key
]?.
enabled
!==
false
return
(
<
div
key=
{
section
.
key
}
className=
'space-y-3'
>
<
div
className=
'bg-muted/50 flex items-center justify-between rounded-lg border p-3'
>
<
div
>
<
div
key=
{
section
.
key
}
className=
'bg-background/60 rounded-xl border p-3'
>
<
div
className=
'flex items-start justify-between gap-3'
>
<
div
className=
'min-w-0'
>
<
p
className=
'text-sm font-medium'
>
{
section
.
title
}
</
p
>
<
p
className=
'text-muted-foreground text-xs'
>
{
section
.
description
}
...
...
@@ -209,11 +218,11 @@ export function SidebarModulesCard() {
onCheckedChange=
{
(
v
)
=>
toggleSection
(
section
.
key
,
v
)
}
/>
</
div
>
<
div
className=
'
grid grid-cols-2 gap-2 sm:grid-cols-3
'
>
<
div
className=
'
mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-1
'
>
{
section
.
modules
.
map
((
mod
)
=>
(
<
div
key=
{
mod
.
key
}
className=
{
`flex 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 transition-opacity ${
sectionEnabled ? '' : 'opacity-50'
}`
}
>
...
...
@@ -239,7 +248,7 @@ export function SidebarModulesCard() {
)
})
}
<
div
className=
'flex
justify-end gap-2 border-t pt-4
'
>
<
div
className=
'flex
flex-col-reverse gap-2 border-t pt-4 sm:flex-row sm:justify-end
'
>
<
Button
variant=
'outline'
onClick=
{
handleReset
}
>
{
t
(
'Reset to Default'
)
}
</
Button
>
...
...
web/default/src/features/profile/components/tabs/account-bindings-tab.tsx
View file @
f9825448
...
...
@@ -249,9 +249,9 @@ export function AccountBindingsTab({
{
bindings
.
map
((
binding
)
=>
(
<
div
key=
{
binding
.
id
}
className=
'flex
items-center justify-between rounded-lg border p-3
'
className=
'flex
flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between
'
>
<
div
className=
'flex items-center gap-3'
>
<
div
className=
'flex
min-w-0
items-center gap-3'
>
<
div
className=
'bg-muted shrink-0 rounded-md p-2'
>
<
binding
.
icon
className=
'h-4 w-4'
/>
</
div
>
...
...
@@ -274,7 +274,7 @@ export function AccountBindingsTab({
<
Button
variant=
'outline'
size=
'sm'
className=
'
ml-2 h-7 shrink-0 px-2.5 text-xs
'
className=
'
h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto
'
onClick=
{
binding
.
onBind
}
disabled=
{
binding
.
isBound
&&
binding
.
id
!==
'email'
}
>
...
...
@@ -304,9 +304,9 @@ export function AccountBindingsTab({
return
(
<
div
key=
{
provider
.
id
}
className=
'flex
items-center justify-between rounded-lg border p-3
'
className=
'flex
flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between
'
>
<
div
className=
'flex items-center gap-3'
>
<
div
className=
'flex
min-w-0
items-center gap-3'
>
<
div
className=
'bg-muted shrink-0 rounded-md p-2'
>
<
Link2
className=
'h-4 w-4'
/>
</
div
>
...
...
@@ -332,7 +332,7 @@ export function AccountBindingsTab({
<
Button
variant=
'ghost'
size=
'sm'
className=
'text-destructive hover:text-destructive
ml-2 h-7 shrink-0 px-2.5 text-xs
'
className=
'text-destructive hover:text-destructive
h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto
'
onClick=
{
()
=>
setUnbindTarget
(
binding
)
}
>
<
Unlink
className=
'mr-1 h-3 w-3'
/>
...
...
@@ -342,7 +342,7 @@ export function AccountBindingsTab({
<
Button
variant=
'outline'
size=
'sm'
className=
'
ml-2 h-7 shrink-0 px-2.5 text-xs
'
className=
'
h-7 shrink-0 self-start px-2.5 text-xs sm:self-auto
'
onClick=
{
()
=>
handleBindCustomOAuth
(
provider
)
}
>
{
t
(
'Bind'
)
}
...
...
web/default/src/features/profile/components/tabs/notification-tab.tsx
View file @
f9825448
...
...
@@ -132,7 +132,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
className=
'sr-only'
/>
<
Icon
className=
'h-5 w-5'
/>
<
span
className=
'text-sm font-medium'
>
{
method
.
label
}
</
span
>
<
span
className=
'text-sm font-medium'
>
{
t
(
method
.
label
)
}
</
span
>
</
Label
>
)
})
}
...
...
@@ -297,7 +297,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
{
/* Receive Upstream Model Update Notifications (admin only) */
}
{
isAdmin
&&
(
<
div
className=
'flex
items-center justify-between rounded-lg border p-4
'
>
<
div
className=
'flex
flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between
'
>
<
div
className=
'space-y-0.5'
>
<
Label
htmlFor=
'upstreamModelUpdateNotify'
>
{
t
(
'Receive Upstream Model Update Notifications'
)
}
...
...
@@ -310,6 +310,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</
div
>
<
Switch
id=
'upstreamModelUpdateNotify'
className=
'shrink-0'
checked=
{
settings
.
upstream_model_update_notify_enabled
}
onCheckedChange=
{
(
checked
)
=>
updateField
(
'upstream_model_update_notify_enabled'
,
checked
)
...
...
@@ -319,7 +320,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
)
}
{
/* Accept Unset Model Price */
}
<
div
className=
'flex
items-center justify-between rounded-lg border p-4
'
>
<
div
className=
'flex
flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between
'
>
<
div
className=
'space-y-0.5'
>
<
Label
htmlFor=
'acceptUnsetPrice'
>
{
t
(
'Accept Unpriced Models'
)
}
...
...
@@ -330,6 +331,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</
div
>
<
Switch
id=
'acceptUnsetPrice'
className=
'shrink-0'
checked=
{
settings
.
accept_unset_model_ratio_model
}
onCheckedChange=
{
(
checked
)
=>
updateField
(
'accept_unset_model_ratio_model'
,
checked
)
...
...
@@ -338,7 +340,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</
div
>
{
/* Record IP Log */
}
<
div
className=
'flex
items-center justify-between rounded-lg border p-4
'
>
<
div
className=
'flex
flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between
'
>
<
div
className=
'space-y-0.5'
>
<
Label
htmlFor=
'recordIp'
>
{
t
(
'Record IP Address'
)
}
</
Label
>
<
p
className=
'text-muted-foreground text-sm'
>
...
...
@@ -347,6 +349,7 @@ export function NotificationTab({ profile, onUpdate }: NotificationTabProps) {
</
div
>
<
Switch
id=
'recordIp'
className=
'shrink-0'
checked=
{
settings
.
record_ip_log
}
onCheckedChange=
{
(
checked
)
=>
updateField
(
'record_ip_log'
,
checked
)
}
/>
...
...
web/default/src/features/profile/components/two-fa-card.tsx
View file @
f9825448
...
...
@@ -2,7 +2,13 @@ import { Shield, AlertTriangle, RefreshCw } from 'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useDialogs
}
from
'@/hooks/use-dialog'
import
{
Button
}
from
'@/components/ui/button'
import
{
Card
,
CardContent
,
CardHeader
}
from
'@/components/ui/card'
import
{
Card
,
CardContent
,
CardDescription
,
CardHeader
,
CardTitle
,
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
{
StatusBadge
}
from
'@/components/status-badge'
import
{
useTwoFA
}
from
'../hooks'
...
...
@@ -27,7 +33,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
if
(
pageLoading
||
loading
)
{
return
(
<
Card
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
>
<
Skeleton
className=
'h-6 w-48'
/>
<
Skeleton
className=
'mt-2 h-4 w-64'
/>
...
...
@@ -41,20 +47,20 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
return
(
<>
<
Card
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
>
<
h3
className=
'text-xl font-semibold
tracking-tight'
>
<
CardTitle
className=
'text-xl
tracking-tight'
>
{
t
(
'Two-Factor Authentication'
)
}
</
h3
>
<
p
className=
'text-muted-foreground mt-2 text-sm'
>
</
CardTitle
>
<
CardDescription
>
{
t
(
'Add an extra layer of security to your account'
)
}
</
p
>
</
CardDescription
>
</
CardHeader
>
<
CardContent
>
<
div
className=
'space-y-6'
>
{
/* Status Section */
}
<
div
className=
'flex
items-start justify-between
'
>
<
div
className=
'flex
flex-col gap-4 sm:flex-row sm:items-start sm:justify-between xl:flex-col 2xl:flex-row
'
>
<
div
className=
'flex items-start gap-4'
>
<
div
className=
'bg-muted rounded-md p-2'
>
<
Shield
className=
'h-5 w-5'
/>
...
...
@@ -97,7 +103,10 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
</
div
>
{
!
status
.
enabled
&&
(
<
Button
onClick=
{
()
=>
dialogs
.
open
(
'setup'
)
}
>
<
Button
className=
'w-full sm:w-auto xl:w-full 2xl:w-auto'
onClick=
{
()
=>
dialogs
.
open
(
'setup'
)
}
>
{
t
(
'Enable'
)
}
</
Button
>
)
}
...
...
@@ -105,7 +114,7 @@ export function TwoFACard({ loading: pageLoading }: TwoFACardProps) {
{
/* Actions Section - Only show when enabled */
}
{
status
.
enabled
&&
(
<
div
className=
'flex flex-col gap-3 border-t pt-6 sm:flex-row'
>
<
div
className=
'flex flex-col gap-3 border-t pt-6 sm:flex-row
xl:flex-col 2xl:flex-row
'
>
<
Button
variant=
'outline'
className=
'flex-1'
...
...
web/default/src/features/profile/index.tsx
View file @
f9825448
...
...
@@ -30,21 +30,24 @@ export function Profile() {
<>
<
AppHeader
/>
<
Main
>
<
div
className=
'min-h-0 flex-1 overflow-auto px-4 py-6'
>
<
CardStaggerContainer
className=
'
space-y-8
'
>
<
div
className=
'min-h-0 flex-1 overflow-auto px-4 py-
4 sm:py-
6'
>
<
CardStaggerContainer
className=
'
mx-auto flex w-full max-w-7xl flex-col gap-5 sm:gap-6
'
>
<
CardStaggerItem
>
<
ProfileHeader
profile=
{
profile
}
loading=
{
loading
}
/>
</
CardStaggerItem
>
<
CardStaggerItem
>
<
div
className=
'grid gap-6 lg:grid-cols-2 lg:items-start'
>
<
div
className=
'space-y-6'
>
<
div
className=
'grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(360px,0.46fr)] xl:items-start'
>
<
div
className=
'space-y-5 sm:space-y-6'
>
<
ProfileSettingsCard
profile=
{
profile
}
loading=
{
loading
}
onProfileUpdate=
{
refreshProfile
}
/>
<
ProfileSecurityCard
profile=
{
profile
}
loading=
{
loading
}
/>
<
PasskeyCard
loading=
{
loading
}
/>
<
TwoFACard
loading=
{
loading
}
/>
</
div
>
<
div
className=
'space-y-6'
>
<
div
className=
'space-y-
5 sm:space-y-6 xl:sticky xl:top-
6'
>
{
checkinEnabled
&&
(
<
CheckinCalendarCard
checkinEnabled=
{
checkinEnabled
}
...
...
@@ -52,12 +55,9 @@ export function Profile() {
turnstileSiteKey=
{
turnstileSiteKey
}
/>
)
}
<
ProfileSettingsCard
profile=
{
profile
}
loading=
{
loading
}
onProfileUpdate=
{
refreshProfile
}
/>
{
canConfigureSidebar
&&
<
SidebarModulesCard
/>
}
<
PasskeyCard
loading=
{
loading
}
/>
<
TwoFACard
loading=
{
loading
}
/>
</
div
>
</
div
>
</
CardStaggerItem
>
...
...
web/default/src/features/usage-logs/index.tsx
View file @
f9825448
import
{
getRouteApi
}
from
'@tanstack/react-router'
import
{
useCallback
,
useMemo
}
from
'react'
import
{
getRouteApi
,
useNavigate
}
from
'@tanstack/react-router'
import
{
useTranslation
}
from
'react-i18next'
import
{
useSidebarConfig
}
from
'@/hooks/use-sidebar-config'
import
{
Tabs
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
import
{
SectionPageLayout
}
from
'@/components/layout'
import
type
{
NavGroup
}
from
'@/components/layout/types'
import
{
CacheStatsDialog
}
from
'@/features/system-settings/general/channel-affinity/cache-stats-dialog'
import
{
UserInfoDialog
}
from
'./components/dialogs/user-info-dialog'
import
{
UsageLogsPrimaryButtons
}
from
'./components/usage-logs-primary-buttons'
...
...
@@ -16,9 +20,29 @@ import {
}
from
'./section-registry'
const
route
=
getRouteApi
(
'/_authenticated/usage-logs/$section'
)
const
TASK_LOG_SECTIONS
=
[
'drawing'
,
'task'
]
as
const
const
SECTION_META
:
Record
<
UsageLogsSectionId
,
{
titleKey
:
string
;
descriptionKey
:
string
}
>
=
{
common
:
{
titleKey
:
'Common Logs'
,
descriptionKey
:
'View and manage your API usage logs'
,
},
drawing
:
{
titleKey
:
'Drawing Logs'
,
descriptionKey
:
'View and manage your drawing logs'
,
},
task
:
{
titleKey
:
'Task Logs'
,
descriptionKey
:
'View and manage your task logs'
,
},
}
function
UsageLogsContent
()
{
const
{
t
}
=
useTranslation
()
const
navigate
=
useNavigate
()
const
params
=
route
.
useParams
()
const
activeCategory
:
UsageLogsSectionId
=
params
.
section
&&
isUsageLogsSectionId
(
params
.
section
)
...
...
@@ -32,31 +56,54 @@ function UsageLogsContent() {
affinityDialogOpen
,
setAffinityDialogOpen
,
}
=
useUsageLogsContext
()
const
tabNavGroups
=
useMemo
<
NavGroup
[]
>
(
()
=>
[
{
title
:
'Task Logs'
,
items
:
TASK_LOG_SECTIONS
.
map
((
section
)
=>
({
title
:
SECTION_META
[
section
].
titleKey
,
url
:
`/usage-logs/
${
section
}
`
,
})),
},
],
[]
)
const
filteredTabGroups
=
useSidebarConfig
(
tabNavGroups
)
const
visibleSections
=
useMemo
(
()
=>
(
filteredTabGroups
[
0
]?.
items
??
[])
.
map
((
item
)
=>
{
if
(
!
(
'url'
in
item
)
||
typeof
item
.
url
!==
'string'
)
return
null
return
item
.
url
.
split
(
'/'
).
pop
()
??
null
})
.
filter
(
(
section
):
section
is
UsageLogsSectionId
=>
Boolean
(
section
&&
isUsageLogsSectionId
(
section
))
),
[
filteredTabGroups
]
)
const
title
=
activeCategory
===
'common'
?
t
(
'Common Logs'
)
:
activeCategory
===
'drawing'
?
t
(
'Drawing Logs'
)
:
activeCategory
===
'task'
?
t
(
'Task Logs'
)
:
t
(
'Usage Logs'
)
const
handleSectionChange
=
useCallback
(
(
section
:
string
)
=>
{
void
navigate
({
to
:
'/usage-logs/$section'
,
params
:
{
section
:
section
as
UsageLogsSectionId
},
})
},
[
navigate
]
)
const
description
=
activeCategory
===
'common'
?
t
(
'View and manage your API usage logs'
)
:
activeCategory
===
'drawing'
?
t
(
'View and manage your drawing logs'
)
:
activeCategory
===
'task'
?
t
(
'View and manage your task logs'
)
:
t
(
'View and manage your API usage logs'
)
const
pageMeta
=
activeCategory
===
'common'
?
SECTION_META
.
common
:
SECTION_META
.
task
const
showTaskSwitcher
=
activeCategory
!==
'common'
&&
visibleSections
.
length
>
1
return
(
<>
<
SectionPageLayout
>
<
SectionPageLayout
.
Title
>
{
t
itle
}
</
SectionPageLayout
.
Title
>
<
SectionPageLayout
.
Title
>
{
t
(
pageMeta
.
titleKey
)
}
</
SectionPageLayout
.
Title
>
<
SectionPageLayout
.
Description
>
{
description
}
{
t
(
pageMeta
.
descriptionKey
)
}
</
SectionPageLayout
.
Description
>
<
SectionPageLayout
.
Actions
>
{
activeCategory
!==
'common'
&&
(
...
...
@@ -64,7 +111,20 @@ function UsageLogsContent() {
)
}
</
SectionPageLayout
.
Actions
>
<
SectionPageLayout
.
Content
>
<
UsageLogsTable
logCategory=
{
activeCategory
}
/>
<
div
className=
'space-y-4'
>
{
showTaskSwitcher
&&
(
<
Tabs
value=
{
activeCategory
}
onValueChange=
{
handleSectionChange
}
>
<
TabsList
className=
'h-auto max-w-full flex-wrap justify-start'
>
{
visibleSections
.
map
((
section
)
=>
(
<
TabsTrigger
key=
{
section
}
value=
{
section
}
>
{
t
(
SECTION_META
[
section
].
titleKey
)
}
</
TabsTrigger
>
))
}
</
TabsList
>
</
Tabs
>
)
}
<
UsageLogsTable
logCategory=
{
activeCategory
}
/>
</
div
>
</
SectionPageLayout
.
Content
>
</
SectionPageLayout
>
...
...
web/default/src/features/wallet/components/affiliate-rewards-card.tsx
View file @
f9825448
import
{
Share2
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
formatQuota
}
from
'@/lib/format'
import
{
Button
}
from
'@/components/ui/button'
import
{
Card
,
CardContent
,
CardHeader
}
from
'@/components/ui/card'
import
{
Card
,
CardContent
,
CardDescription
,
CardHeader
,
CardTitle
,
}
from
'@/components/ui/card'
import
{
Input
}
from
'@/components/ui/input'
import
{
Label
}
from
'@/components/ui/label'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
...
...
@@ -24,18 +31,18 @@ export function AffiliateRewardsCard({
const
{
t
}
=
useTranslation
()
if
(
loading
)
{
return
(
<
Card
>
<
CardHeader
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
Skeleton
className=
'h-6 w-32'
/>
<
Skeleton
className=
'mt-2 h-4 w-48'
/>
</
CardHeader
>
<
CardContent
className=
'space-y-
8
'
>
<
CardContent
className=
'space-y-
6 pt-6
'
>
{
/* Statistics Skeleton */
}
<
div
className=
'grid grid-cols-1 gap-
4 sm:grid-cols-3 sm:gap-6
'
>
<
div
className=
'grid grid-cols-1 gap-
3
'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
i
}
className=
'
space-y-2
'
>
<
div
key=
{
i
}
className=
'
rounded-lg border p-3
'
>
<
Skeleton
className=
'h-3 w-16'
/>
<
Skeleton
className=
'h-8 w-24'
/>
<
Skeleton
className=
'
mt-2
h-8 w-24'
/>
</
div
>
))
}
</
div
>
...
...
@@ -59,41 +66,50 @@ export function AffiliateRewardsCard({
const
hasRewards
=
(
user
?.
aff_quota
??
0
)
>
0
return
(
<
Card
>
<
CardHeader
>
<
h3
className=
'text-xl font-semibold tracking-tight'
>
{
t
(
'Referral Program'
)
}
</
h3
>
<
p
className=
'text-muted-foreground mt-2 text-sm'
>
{
t
(
'Share your link and earn rewards'
)
}
</
p
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
div
className=
'flex items-center gap-3'
>
<
div
className=
'bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'
>
<
Share2
className=
'h-4 w-4'
/>
</
div
>
<
div
className=
'min-w-0'
>
<
CardTitle
className=
'text-xl tracking-tight'
>
{
t
(
'Referral Program'
)
}
</
CardTitle
>
<
CardDescription
>
{
t
(
'Share your link and earn rewards'
)
}
</
CardDescription
>
</
div
>
</
div
>
</
CardHeader
>
<
CardContent
className=
'space-y-
8
'
>
<
CardContent
className=
'space-y-
6 pt-6
'
>
{
/* Statistics */
}
<
div
className=
'grid grid-cols-1 gap-
4 sm:grid-cols-3 sm:gap-6
'
>
<
div
className=
'
space-y-2
'
>
<
div
className=
'grid grid-cols-1 gap-
3 sm:grid-cols-3 xl:grid-cols-1
'
>
<
div
className=
'
rounded-lg border p-3
'
>
<
div
className=
'text-muted-foreground text-xs font-medium tracking-wider uppercase'
>
{
t
(
'Pending'
)
}
</
div
>
<
div
className=
'
text-2xl font-semibold
'
>
<
div
className=
'
mt-2 text-2xl font-semibold break-all
'
>
{
formatQuota
(
user
?.
aff_quota
??
0
)
}
</
div
>
</
div
>
<
div
className=
'
space-y-2
'
>
<
div
className=
'
rounded-lg border p-3
'
>
<
div
className=
'text-muted-foreground text-xs font-medium tracking-wider uppercase'
>
{
t
(
'Total Earned'
)
}
</
div
>
<
div
className=
'
text-2xl font-semibold
'
>
<
div
className=
'
mt-2 text-2xl font-semibold break-all
'
>
{
formatQuota
(
user
?.
aff_history_quota
??
0
)
}
</
div
>
</
div
>
<
div
className=
'
space-y-2
'
>
<
div
className=
'
rounded-lg border p-3
'
>
<
div
className=
'text-muted-foreground text-xs font-medium tracking-wider uppercase'
>
{
t
(
'Invites'
)
}
</
div
>
<
div
className=
'text-2xl font-semibold'
>
{
user
?.
aff_count
??
0
}
</
div
>
<
div
className=
'mt-2 text-2xl font-semibold'
>
{
user
?.
aff_count
??
0
}
</
div
>
</
div
>
</
div
>
...
...
web/default/src/features/wallet/components/recharge-form-card.tsx
View file @
f9825448
import
{
useState
,
useEffect
}
from
'react'
import
{
Gift
,
ExternalLink
,
Loader2
,
Receipt
}
from
'lucide-react'
import
{
Gift
,
ExternalLink
,
Loader2
,
Receipt
,
WalletCards
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
formatNumber
}
from
'@/lib/format'
import
{
cn
}
from
'@/lib/utils'
import
{
Alert
,
AlertDescription
}
from
'@/components/ui/alert'
import
{
Button
}
from
'@/components/ui/button'
import
{
Card
,
CardContent
,
CardHeader
}
from
'@/components/ui/card'
import
{
Card
,
CardContent
,
CardDescription
,
CardHeader
,
CardTitle
,
}
from
'@/components/ui/card'
import
{
Input
}
from
'@/components/ui/input'
import
{
Label
}
from
'@/components/ui/label'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
...
...
@@ -119,12 +125,12 @@ export function RechargeFormCard({
if
(
loading
)
{
return
(
<
Card
>
<
CardHeader
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
Skeleton
className=
'h-6 w-32'
/>
<
Skeleton
className=
'mt-2 h-4 w-48'
/>
</
CardHeader
>
<
CardContent
className=
'space-y-
8
'
>
<
CardContent
className=
'space-y-
6 pt-6
'
>
<
div
className=
'space-y-6'
>
{
/* Preset Amounts Skeleton */
}
<
div
className=
'space-y-3'
>
...
...
@@ -167,31 +173,36 @@ export function RechargeFormCard({
}
return
(
<
Card
>
<
CardHeader
>
<
div
className=
'flex items-center justify-between'
>
<
div
>
<
h3
className=
'text-xl font-semibold tracking-tight'
>
{
t
(
'Add Funds'
)
}
</
h3
>
<
p
className=
'text-muted-foreground mt-2 text-sm'
>
{
t
(
'Choose an amount and payment method'
)
}
</
p
>
<
Card
className=
'overflow-hidden'
>
<
CardHeader
className=
'border-b'
>
<
div
className=
'flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'
>
<
div
className=
'flex min-w-0 items-center gap-3'
>
<
div
className=
'bg-muted flex h-9 w-9 shrink-0 items-center justify-center rounded-lg'
>
<
WalletCards
className=
'h-4 w-4'
/>
</
div
>
<
div
className=
'min-w-0'
>
<
CardTitle
className=
'text-xl tracking-tight'
>
{
t
(
'Add Funds'
)
}
</
CardTitle
>
<
CardDescription
>
{
t
(
'Choose an amount and payment method'
)
}
</
CardDescription
>
</
div
>
</
div
>
{
onOpenBilling
&&
(
<
Button
variant=
'outline'
size=
'sm'
onClick=
{
onOpenBilling
}
className=
'
gap-2
'
className=
'
w-full gap-2 sm:w-auto
'
>
<
Receipt
className=
'h-4 w-4'
/>
{
t
(
'
Billing
'
)
}
{
t
(
'
Order History
'
)
}
</
Button
>
)
}
</
div
>
</
CardHeader
>
<
CardContent
className=
'space-y-
8
'
>
<
CardContent
className=
'space-y-
6 pt-6
'
>
{
/* Online Topup Section */
}
{
hasAnyTopup
?
(
<
div
className=
'space-y-6'
>
...
...
@@ -202,7 +213,7 @@ export function RechargeFormCard({
<
Label
className=
'text-muted-foreground text-xs font-medium tracking-wider uppercase'
>
{
t
(
'Amount'
)
}
</
Label
>
<
div
className=
'grid grid-cols-2 gap-3
sm
:grid-cols-4'
>
<
div
className=
'grid grid-cols-2 gap-3
md
:grid-cols-4'
>
{
presetAmounts
.
map
((
preset
,
index
)
=>
{
const
discount
=
preset
.
discount
||
...
...
@@ -224,7 +235,7 @@ export function RechargeFormCard({
key=
{
index
}
variant=
'outline'
className=
{
cn
(
'hover:border-foreground
h-auto
rounded-lg p-4 text-left whitespace-normal'
,
'hover:border-foreground
flex h-auto flex-col items-start
rounded-lg p-4 text-left whitespace-normal'
,
selectedPreset
===
preset
.
value
?
'border-foreground bg-foreground/5'
:
'border-muted'
...
...
@@ -264,7 +275,7 @@ export function RechargeFormCard({
>
{
t
(
'Custom Amount'
)
}
</
Label
>
<
div
className=
'
relative
'
>
<
div
className=
'
grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center
'
>
<
Input
id=
'topup-amount'
type=
'number'
...
...
@@ -272,9 +283,9 @@ export function RechargeFormCard({
onChange=
{
(
e
)
=>
handleAmountChange
(
e
.
target
.
value
)
}
min=
{
minTopup
}
placeholder=
{
`Minimum ${minTopup}`
}
className=
'
pr-32
text-lg'
className=
'text-lg'
/>
<
div
className=
'
absolute end-3 top-1/2 flex -translate-y-1/2 items-center gap-
2'
>
<
div
className=
'
bg-muted/30 flex min-h-10 items-center justify-between gap-3 rounded-md border px-3 lg:min-w-5
2'
>
<
span
className=
'text-muted-foreground text-xs'
>
{
t
(
'Amount to pay:'
)
}
</
span
>
...
...
@@ -294,7 +305,7 @@ export function RechargeFormCard({
{
t
(
'Payment Method'
)
}
</
Label
>
{
hasStandardPaymentMethods
?
(
<
div
className=
'
flex flex-wrap gap
-3'
>
<
div
className=
'
grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols
-3'
>
{
topupInfo
?.
pay_methods
?.
map
((
method
)
=>
{
const
minTopup
=
method
.
min_topup
||
0
const
disabled
=
minTopup
>
topupAmount
...
...
@@ -305,7 +316,7 @@ export function RechargeFormCard({
variant=
'outline'
onClick=
{
()
=>
onPaymentMethodSelect
(
method
)
}
disabled=
{
disabled
||
!!
paymentLoading
}
className=
'gap-2 rounded-lg'
className=
'
justify-start
gap-2 rounded-lg'
>
{
paymentLoading
===
method
.
type
?
(
<
Loader2
className=
'h-4 w-4 animate-spin'
/>
...
...
@@ -355,7 +366,7 @@ export function RechargeFormCard({
<
Label
className=
'text-muted-foreground text-xs font-medium tracking-wider uppercase'
>
{
t
(
'Waffo Payment'
)
}
</
Label
>
<
div
className=
'
flex flex-wrap gap
-3'
>
<
div
className=
'
grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols
-3'
>
{
waffoPayMethods
?.
map
((
method
,
index
)
=>
{
const
loadingKey
=
`waffo-${index}`
const
waffoMin
=
waffoMinTopup
||
0
...
...
@@ -367,7 +378,7 @@ export function RechargeFormCard({
variant=
'outline'
onClick=
{
()
=>
onWaffoMethodSelect
(
method
,
index
)
}
disabled=
{
belowMin
||
!!
paymentLoading
}
className=
'gap-2 rounded-lg'
className=
'
justify-start
gap-2 rounded-lg'
>
{
paymentLoading
===
loadingKey
?
(
<
Loader2
className=
'h-4 w-4 animate-spin'
/>
...
...
@@ -434,7 +445,7 @@ export function RechargeFormCard({
)
}
{
/* Redemption Code Section */
}
<
div
className=
'space-y-3 border-t pt-
8
'
>
<
div
className=
'space-y-3 border-t pt-
6
'
>
<
div
className=
'flex items-center gap-2'
>
<
Gift
className=
'text-muted-foreground h-4 w-4'
/>
<
Label
...
...
@@ -444,7 +455,7 @@ export function RechargeFormCard({
{
t
(
'Have a Code?'
)
}
</
Label
>
</
div
>
<
div
className=
'flex
gap-2
'
>
<
div
className=
'flex
flex-col gap-2 sm:flex-row
'
>
<
Input
id=
'redemption-code'
value=
{
redemptionCode
}
...
...
@@ -452,7 +463,12 @@ export function RechargeFormCard({
placeholder=
{
t
(
'Enter your redemption code'
)
}
className=
'flex-1'
/>
<
Button
onClick=
{
onRedeem
}
disabled=
{
redeeming
}
variant=
'outline'
>
<
Button
onClick=
{
onRedeem
}
disabled=
{
redeeming
}
variant=
'outline'
className=
'sm:w-auto'
>
{
redeeming
&&
<
Loader2
className=
'mr-2 h-4 w-4 animate-spin'
/>
}
{
t
(
'Redeem'
)
}
</
Button
>
...
...
web/default/src/features/wallet/components/subscription-plans-card.tsx
View file @
f9825448
This diff is collapsed.
Click to expand it.
web/default/src/features/wallet/components/wallet-stats-card.tsx
View file @
f9825448
import
{
Activity
,
BarChart3
,
WalletCards
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
formatQuota
}
from
'@/lib/format'
import
{
cn
}
from
'@/lib/utils'
import
{
Card
,
CardContent
}
from
'@/components/ui/card'
import
{
Skeleton
}
from
'@/components/ui/skeleton'
import
type
{
UserWalletData
}
from
'../types'
...
...
@@ -13,13 +15,21 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
const
{
t
}
=
useTranslation
()
if
(
props
.
loading
)
{
return
(
<
Card
>
<
CardContent
>
<
div
className=
'grid grid-cols-1
gap-6 sm:grid-cols-3 sm:gap-8
'
>
<
Card
className=
'overflow-hidden'
>
<
CardContent
className=
'p-0'
>
<
div
className=
'grid grid-cols-1
sm:grid-cols-3
'
>
{
Array
.
from
({
length
:
3
}).
map
((
_
,
i
)
=>
(
<
div
key=
{
i
}
className=
'space-y-2'
>
<
Skeleton
className=
'h-5 w-28'
/>
<
Skeleton
className=
'h-11 w-32'
/>
<
div
key=
{
i
}
className=
{
cn
(
'flex items-center justify-center px-4 py-3 sm:px-5 sm:py-4'
,
i
>
0
&&
'border-t sm:border-t-0 sm:border-l'
)
}
>
<
div
className=
'w-full max-w-44'
>
<
Skeleton
className=
'h-4 w-24'
/>
<
Skeleton
className=
'mt-2 h-7 w-32'
/>
</
div
>
</
div
>
))
}
</
div
>
...
...
@@ -28,39 +38,47 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
)
}
return
(
<
Card
>
<
CardContent
>
<
div
className=
'grid grid-cols-1 gap-6 sm:grid-cols-3 sm:gap-8'
>
{
/* Current Balance */
}
<
div
className=
'min-w-0 space-y-2'
>
<
div
className=
'text-muted-foreground text-sm font-medium'
>
{
t
(
'Current Balance'
)
}
</
div
>
<
div
className=
'text-3xl leading-tight font-semibold tracking-tight break-all lg:text-4xl'
>
{
formatQuota
(
props
.
user
?.
quota
??
0
)
}
</
div
>
</
div
>
{
/* Total Usage */
}
<
div
className=
'min-w-0 space-y-2'
>
<
div
className=
'text-muted-foreground text-sm font-medium'
>
{
t
(
'Total Usage'
)
}
</
div
>
<
div
className=
'text-3xl leading-tight font-semibold tracking-tight break-all lg:text-4xl'
>
{
formatQuota
(
props
.
user
?.
used_quota
??
0
)
}
</
div
>
</
div
>
const
stats
=
[
{
label
:
t
(
'Current Balance'
),
value
:
formatQuota
(
props
.
user
?.
quota
??
0
),
icon
:
WalletCards
,
},
{
label
:
t
(
'Total Usage'
),
value
:
formatQuota
(
props
.
user
?.
used_quota
??
0
),
icon
:
BarChart3
,
},
{
label
:
t
(
'API Requests'
),
value
:
(
props
.
user
?.
request_count
??
0
).
toLocaleString
(),
icon
:
Activity
,
},
]
{
/* Request Count */
}
<
div
className=
'min-w-0 space-y-2'
>
<
div
className=
'text-muted-foreground text-sm font-medium'
>
{
t
(
'API Requests'
)
}
</
div
>
<
div
className=
'text-3xl leading-tight font-semibold tracking-tight break-all lg:text-4xl'
>
{
(
props
.
user
?.
request_count
??
0
).
toLocaleString
()
}
return
(
<
Card
className=
'overflow-hidden'
>
<
CardContent
className=
'p-0'
>
<
div
className=
'grid grid-cols-1 sm:grid-cols-3'
>
{
stats
.
map
((
item
,
index
)
=>
(
<
div
key=
{
item
.
label
}
className=
{
cn
(
'flex min-w-0 justify-center px-4 py-3 sm:px-5 sm:py-4'
,
index
>
0
&&
'border-t sm:border-t-0 sm:border-l'
)
}
>
<
div
className=
'min-w-0 text-center'
>
<
div
className=
'text-muted-foreground flex items-center justify-center gap-1.5 text-xs font-medium'
>
<
item
.
icon
className=
'h-3.5 w-3.5'
/>
{
item
.
label
}
</
div
>
<
div
className=
'mt-1 text-xl leading-tight font-semibold tracking-tight break-all lg:text-2xl'
>
{
item
.
value
}
</
div
>
</
div
>
</
div
>
</
div
>
))
}
</
div
>
</
CardContent
>
</
Card
>
...
...
web/default/src/features/wallet/index.tsx
View file @
f9825448
...
...
@@ -239,54 +239,56 @@ export function Wallet(props: WalletProps) {
{
t
(
'Manage your balance and payment methods'
)
}
</
SectionPageLayout
.
Description
>
<
SectionPageLayout
.
Content
>
<
div
className=
'grid gap-6 lg:grid-cols-3'
>
{
/* Left Column - Stats & Recharge */
}
<
div
className=
'space-y-6 lg:col-span-2'
>
<
WalletStatsCard
user=
{
user
}
loading=
{
userLoading
}
/>
<
RechargeFormCard
topupInfo=
{
topupInfo
}
presetAmounts=
{
presetAmounts
}
selectedPreset=
{
selectedPreset
}
onSelectPreset=
{
handleSelectPreset
}
topupAmount=
{
topupAmount
}
onTopupAmountChange=
{
handleTopupAmountChange
}
paymentAmount=
{
paymentAmount
}
calculating=
{
calculating
}
onPaymentMethodSelect=
{
handlePaymentMethodSelect
}
paymentLoading=
{
paymentLoading
}
redemptionCode=
{
redemptionCode
}
onRedemptionCodeChange=
{
setRedemptionCode
}
onRedeem=
{
handleRedeem
}
redeeming=
{
redeeming
}
topupLink=
{
topupInfo
?.
topup_link
}
loading=
{
topupLoading
}
priceRatio=
{
(
status
?.
price
as
number
)
||
1
}
usdExchangeRate=
{
effectiveUsdExchangeRate
}
onOpenBilling=
{
()
=>
setBillingDialogOpen
(
true
)
}
creemProducts=
{
topupInfo
?.
creem_products
}
enableCreemTopup=
{
topupInfo
?.
enable_creem_topup
}
onCreemProductSelect=
{
handleCreemProductSelect
}
enableWaffoTopup=
{
topupInfo
?.
enable_waffo_topup
}
waffoPayMethods=
{
topupInfo
?.
waffo_pay_methods
}
waffoMinTopup=
{
topupInfo
?.
waffo_min_topup
}
onWaffoMethodSelect=
{
handleWaffoMethodSelect
}
enableWaffoPancakeTopup=
{
topupInfo
?.
enable_waffo_pancake_topup
}
/>
</
div
>
{
/* Right Column - Affiliate & Subscriptions */
}
<
div
className=
'space-y-6 lg:col-span-1'
>
<
AffiliateRewardsCard
user=
{
user
}
affiliateLink=
{
affiliateLink
}
onTransfer=
{
()
=>
setTransferDialogOpen
(
true
)
}
loading=
{
affiliateLoading
}
/>
<
div
className=
'mx-auto flex w-full max-w-7xl flex-col gap-4'
>
<
WalletStatsCard
user=
{
user
}
loading=
{
userLoading
}
/>
<
SubscriptionPlansCard
topupInfo=
{
topupInfo
}
/>
<
div
className=
'grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(340px,0.4fr)] xl:items-start'
>
<
div
className=
'min-w-0'
>
<
RechargeFormCard
topupInfo=
{
topupInfo
}
presetAmounts=
{
presetAmounts
}
selectedPreset=
{
selectedPreset
}
onSelectPreset=
{
handleSelectPreset
}
topupAmount=
{
topupAmount
}
onTopupAmountChange=
{
handleTopupAmountChange
}
paymentAmount=
{
paymentAmount
}
calculating=
{
calculating
}
onPaymentMethodSelect=
{
handlePaymentMethodSelect
}
paymentLoading=
{
paymentLoading
}
redemptionCode=
{
redemptionCode
}
onRedemptionCodeChange=
{
setRedemptionCode
}
onRedeem=
{
handleRedeem
}
redeeming=
{
redeeming
}
topupLink=
{
topupInfo
?.
topup_link
}
loading=
{
topupLoading
}
priceRatio=
{
(
status
?.
price
as
number
)
||
1
}
usdExchangeRate=
{
effectiveUsdExchangeRate
}
onOpenBilling=
{
()
=>
setBillingDialogOpen
(
true
)
}
creemProducts=
{
topupInfo
?.
creem_products
}
enableCreemTopup=
{
topupInfo
?.
enable_creem_topup
}
onCreemProductSelect=
{
handleCreemProductSelect
}
enableWaffoTopup=
{
topupInfo
?.
enable_waffo_topup
}
waffoPayMethods=
{
topupInfo
?.
waffo_pay_methods
}
waffoMinTopup=
{
topupInfo
?.
waffo_min_topup
}
onWaffoMethodSelect=
{
handleWaffoMethodSelect
}
enableWaffoPancakeTopup=
{
topupInfo
?.
enable_waffo_pancake_topup
}
/>
</
div
>
<
div
className=
'xl:sticky xl:top-6'
>
<
AffiliateRewardsCard
user=
{
user
}
affiliateLink=
{
affiliateLink
}
onTransfer=
{
()
=>
setTransferDialogOpen
(
true
)
}
loading=
{
affiliateLoading
}
/>
</
div
>
</
div
>
</
div
>
{
/* Subscription Plans */
}
<
SubscriptionPlansCard
topupInfo=
{
topupInfo
}
/>
</
SectionPageLayout
.
Content
>
</
SectionPageLayout
>
...
...
web/default/src/hooks/use-sidebar-config.ts
View file @
f9825448
...
...
@@ -55,7 +55,9 @@ const URL_TO_CONFIG_MAP: Record<string, { section: string; module: string }> = {
'/dashboard'
:
{
section
:
'console'
,
module
:
'detail'
},
'/dashboard/overview'
:
{
section
:
'console'
,
module
:
'detail'
},
'/dashboard/models'
:
{
section
:
'console'
,
module
:
'detail'
},
'/dashboard/users'
:
{
section
:
'console'
,
module
:
'detail'
},
'/keys'
:
{
section
:
'console'
,
module
:
'token'
},
'/usage-logs'
:
{
section
:
'console'
,
module
:
'log'
},
'/usage-logs/common'
:
{
section
:
'console'
,
module
:
'log'
},
'/usage-logs/drawing'
:
{
section
:
'console'
,
module
:
'midjourney'
},
'/usage-logs/task'
:
{
section
:
'console'
,
module
:
'task'
},
...
...
@@ -173,7 +175,10 @@ function isNavItemVisible(
// Handle direct link type
if
(
'url'
in
item
&&
item
.
url
)
{
return
isModuleEnabled
(
item
.
url
as
string
,
adminConfig
,
userConfig
)
const
configUrls
=
item
.
configUrls
??
[
item
.
url
]
return
configUrls
.
some
((
url
)
=>
isModuleEnabled
(
url
as
string
,
adminConfig
,
userConfig
)
)
}
// Handle collapsible type (with sub-items)
...
...
web/default/src/hooks/use-sidebar-data.ts
View file @
f9825448
import
{
LayoutDashboard
,
Activity
,
Key
,
FileText
,
Wallet
,
...
...
@@ -12,19 +13,14 @@ import {
FlaskConical
,
MessageSquare
,
CreditCard
,
ListTodo
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useAuthStore
}
from
'@/stores/auth-store'
import
{
WORKSPACE_IDS
}
from
'@/components/layout/lib/workspace-registry'
import
{
type
SidebarData
}
from
'@/components/layout/types'
import
{
getDashboardSectionNavItems
}
from
'@/features/dashboard/section-registry'
import
{
getModelsSectionNavItems
}
from
'@/features/models/section-registry'
import
{
getUsageLogsSectionNavItems
}
from
'@/features/usage-logs/section-registry'
export
function
useSidebarData
():
SidebarData
{
const
{
t
}
=
useTranslation
()
const
user
=
useAuthStore
((
s
)
=>
s
.
auth
.
user
)
const
isAdmin
=
Boolean
(
user
?.
role
&&
user
.
role
>=
10
)
return
{
workspaces
:
[
...
...
@@ -57,9 +53,14 @@ export function useSidebarData(): SidebarData {
title
:
t
(
'General'
),
items
:
[
{
title
:
t
(
'Overview'
),
url
:
'/dashboard/overview'
,
icon
:
Activity
,
},
{
title
:
t
(
'Dashboard'
),
url
:
'/dashboard/models'
,
icon
:
LayoutDashboard
,
items
:
getDashboardSectionNavItems
(
t
,
{
isAdmin
}),
},
{
title
:
t
(
'API Keys'
),
...
...
@@ -68,8 +69,15 @@ export function useSidebarData(): SidebarData {
},
{
title
:
t
(
'Usage Logs'
),
url
:
'/usage-logs/common'
,
icon
:
FileText
,
items
:
getUsageLogsSectionNavItems
(
t
),
},
{
title
:
t
(
'Task Logs'
),
url
:
'/usage-logs/task'
,
activeUrls
:
[
'/usage-logs/drawing'
],
configUrls
:
[
'/usage-logs/drawing'
,
'/usage-logs/task'
],
icon
:
ListTodo
,
},
{
title
:
t
(
'Wallet'
),
...
...
@@ -94,8 +102,8 @@ export function useSidebarData(): SidebarData {
},
{
title
:
t
(
'Models'
),
url
:
'/models/metadata'
,
icon
:
Box
,
items
:
getModelsSectionNavItems
(
t
),
},
{
title
:
t
(
'Users'
),
...
...
web/default/src/i18n/locales/en.json
View file @
f9825448
...
...
@@ -2301,6 +2301,7 @@
"Or continue with"
:
"Or continue with"
,
"Or enter this key manually:"
:
"Or enter this key manually:"
,
"Order completed successfully"
:
"Order completed successfully"
,
"Order History"
:
"Order History"
,
"Order Payment Method"
:
"Order Payment Method"
,
"org-..."
:
"org-..."
,
"Original Model"
:
"Original Model"
,
...
...
web/default/src/i18n/locales/fr.json
View file @
f9825448
...
...
@@ -2301,6 +2301,7 @@
"Or continue with"
:
"Ou continuer avec"
,
"Or enter this key manually:"
:
"Ou entrez cette clé manuellement :"
,
"Order completed successfully"
:
"Commande terminée avec succès"
,
"Order History"
:
"Historique des commandes"
,
"Order Payment Method"
:
"Moyen de paiement (commande)"
,
"org-..."
:
"org-..."
,
"Original Model"
:
"Modèle Original"
,
...
...
web/default/src/i18n/locales/ja.json
View file @
f9825448
...
...
@@ -2301,6 +2301,7 @@
"Or continue with"
:
"または、以下で続行"
,
"Or enter this key manually:"
:
"または、このキーを手動で入力してください:"
,
"Order completed successfully"
:
"注文が正常に完了しました"
,
"Order History"
:
"注文履歴"
,
"Order Payment Method"
:
"注文の支払い方法"
,
"org-..."
:
"org-..."
,
"Original Model"
:
"オリジナルモデル"
,
...
...
web/default/src/i18n/locales/ru.json
View file @
f9825448
...
...
@@ -2301,6 +2301,7 @@
"Or continue with"
:
"Или продолжить с"
,
"Or enter this key manually:"
:
"Или введите этот ключ вручную:"
,
"Order completed successfully"
:
"Заказ успешно завершен"
,
"Order History"
:
"История заказов"
,
"Order Payment Method"
:
"Способ оплаты (заказа)"
,
"org-..."
:
"орг-..."
,
"Original Model"
:
"Оригинальная модель"
,
...
...
web/default/src/i18n/locales/vi.json
View file @
f9825448
...
...
@@ -2301,6 +2301,7 @@
"Or continue with"
:
"Hoặc tiếp tục với"
,
"Or enter this key manually:"
:
"Hoặc nhập khóa này thủ công:"
,
"Order completed successfully"
:
"Đơn hàng đã hoàn thành thành công"
,
"Order History"
:
"Lịch sử đơn hàng"
,
"Order Payment Method"
:
"Phương thức thanh toán đơn hàng"
,
"org-..."
:
"org-..."
,
"Original Model"
:
"Nguyên mẫu"
,
...
...
web/default/src/i18n/locales/zh.json
View file @
f9825448
...
...
@@ -2301,6 +2301,7 @@
"Or continue with"
:
"或继续使用"
,
"Or enter this key manually:"
:
"或手动输入此密钥:"
,
"Order completed successfully"
:
"订单已成功完成"
,
"Order History"
:
"订单历史"
,
"Order Payment Method"
:
"订单支付方式"
,
"org-..."
:
"org-..."
,
"Original Model"
:
"原始模型"
,
...
...
web/default/src/styles/theme.css
View file @
f9825448
...
...
@@ -38,39 +38,39 @@
}
.dark
{
--background
:
oklch
(
0.
165
0.012
258
);
--foreground
:
oklch
(
0.
92
0.008
247.858
);
--card
:
oklch
(
0.2
05
0.012
258
);
--card-foreground
:
oklch
(
0.
92
0.008
247.858
);
--popover
:
oklch
(
0.
225
0.014
258
);
--popover-foreground
:
oklch
(
0.
92
0.008
247.858
);
--primary
:
oklch
(
0.
87
0.018
255.508
);
--primary-foreground
:
oklch
(
0.
235
0.042
265.755
);
--secondary
:
oklch
(
0.
255
0.012
258
);
--secondary-foreground
:
oklch
(
0.
92
0.008
247.858
);
--muted
:
oklch
(
0.
245
0.012
258
);
--muted-foreground
:
oklch
(
0.
68
0.014
257.417
);
--accent
:
oklch
(
0.
265
0.014
258
);
--accent-foreground
:
oklch
(
0.9
2
0.008
247.858
);
--background
:
oklch
(
0.
245
0.018
265
);
--foreground
:
oklch
(
0.
88
0.014
252
);
--card
:
oklch
(
0.2
75
0.017
265
);
--card-foreground
:
oklch
(
0.
88
0.014
252
);
--popover
:
oklch
(
0.
3
0.018
265
);
--popover-foreground
:
oklch
(
0.
88
0.014
252
);
--primary
:
oklch
(
0.
68
0.12
236
);
--primary-foreground
:
oklch
(
0.
985
0.004
247.858
);
--secondary
:
oklch
(
0.
32
0.016
265
);
--secondary-foreground
:
oklch
(
0.
88
0.014
252
);
--muted
:
oklch
(
0.
305
0.016
265
);
--muted-foreground
:
oklch
(
0.
72
0.018
252
);
--accent
:
oklch
(
0.
34
0.024
255
);
--accent-foreground
:
oklch
(
0.9
0.012
252
);
--destructive
:
oklch
(
0.704
0.191
22.216
);
--border
:
oklch
(
0.3
1
0.014
258
);
--input
:
oklch
(
0.
34
0.014
258
);
--ring
:
oklch
(
0.
58
0.025
256.788
);
--border
:
oklch
(
0.3
8
0.018
265
);
--input
:
oklch
(
0.
405
0.018
265
);
--ring
:
oklch
(
0.
62
0.09
236
);
--chart-1
:
oklch
(
0.488
0.243
264.376
);
--chart-2
:
oklch
(
0.696
0.17
162.48
);
--chart-3
:
oklch
(
0.769
0.188
70.08
);
--chart-4
:
oklch
(
0.627
0.265
303.9
);
--chart-5
:
oklch
(
0.645
0.246
16.439
);
--sidebar
:
oklch
(
0.
185
0.012
258
);
--sidebar-foreground
:
oklch
(
0.
92
0.008
247.858
);
--sidebar-primary
:
oklch
(
0.75
0.14
233
);
--sidebar-primary-foreground
:
oklch
(
0.29
0.06
243
);
--sidebar-accent
:
oklch
(
0.
255
0.012
258
);
--sidebar-accent-foreground
:
oklch
(
0.9
2
0.008
247.858
);
--sidebar-border
:
oklch
(
0.3
0.014
258
);
--sidebar-ring
:
oklch
(
0.52
0.02
256.788
);
--skeleton-base
:
oklch
(
0.
245
0.01
258
);
--skeleton-highlight
:
oklch
(
0.3
2
0.014
258
);
--sidebar
:
oklch
(
0.
255
0.017
265
);
--sidebar-foreground
:
oklch
(
0.
86
0.014
252
);
--sidebar-primary
:
var
(
--primary
);
--sidebar-primary-foreground
:
var
(
--primary-foreground
);
--sidebar-accent
:
oklch
(
0.
325
0.018
265
);
--sidebar-accent-foreground
:
oklch
(
0.9
0.012
252
);
--sidebar-border
:
oklch
(
0.3
7
0.018
265
);
--sidebar-ring
:
var
(
--ring
);
--skeleton-base
:
oklch
(
0.
31
0.014
265
);
--skeleton-highlight
:
oklch
(
0.3
9
0.018
265
);
}
@theme
inline
{
...
...
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