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