From d46df94f0515b9c898ff8ab3aeb57506c505d9f1 Mon Sep 17 00:00:00 2001 From: CaIon Date: Thu, 30 Apr 2026 19:53:02 +0800 Subject: [PATCH] feat(ui): improve mobile responsive layouts --- .../data-table/mobile-card-list.tsx | 8 +- .../src/components/data-table/pagination.tsx | 10 +- .../src/components/data-table/toolbar.tsx | 10 +- .../components/data-table/view-options.tsx | 4 +- .../layout/components/section-page-layout.tsx | 16 +- web/default/src/components/ui/titled-card.tsx | 81 +++++++++ .../features/auth/hooks/use-auth-redirect.ts | 22 ++- .../channels/components/channels-table.tsx | 7 +- .../drawers/channel-mutate-drawer.tsx | 10 +- .../models/consumption-distribution-chart.tsx | 8 +- .../components/models/log-stat-cards.tsx | 11 +- .../components/models/model-charts.tsx | 8 +- .../models/models-filter-dialog.tsx | 18 +- .../overview/announcements-panel.tsx | 8 +- .../components/overview/api-info-item.tsx | 8 +- .../components/overview/api-info-panel.tsx | 6 +- .../components/overview/faq-panel.tsx | 4 +- .../components/overview/summary-cards.tsx | 13 +- .../components/overview/uptime-panel.tsx | 10 +- .../dashboard/components/ui/panel-wrapper.tsx | 12 +- .../dashboard/components/ui/stat-card.tsx | 22 +-- .../components/users/user-charts.tsx | 23 ++- .../dashboard/hooks/use-dashboard-config.tsx | 8 +- web/default/src/features/dashboard/index.tsx | 8 +- .../src/features/dashboard/lib/filters.ts | 4 +- .../components/api-key-group-combobox.tsx | 18 +- .../components/api-keys-mutate-drawer.tsx | 57 +++--- .../keys/components/api-keys-table.tsx | 151 ++++++++++++++-- .../models/components/deployments-table.tsx | 4 +- .../dialogs/update-config-dialog.tsx | 14 +- .../dialogs/view-details-dialog.tsx | 10 +- .../components/dialogs/view-logs-dialog.tsx | 12 +- .../drawers/model-mutate-drawer.tsx | 8 +- .../drawers/prefill-group-form-drawer.tsx | 10 +- .../models/components/models-table.tsx | 7 +- .../components/dynamic-pricing-breakdown.tsx | 68 ++++++- .../pricing/components/filter-bar.tsx | 12 +- .../pricing/components/model-card.tsx | 6 +- .../pricing/components/model-details.tsx | 4 +- .../pricing/components/pricing-toolbar.tsx | 6 +- web/default/src/features/pricing/index.tsx | 10 +- web/default/src/features/profile/api.ts | 8 + .../components/language-preferences-card.tsx | 136 ++++++++++++++ .../profile/components/passkey-card.tsx | 162 +++++++++-------- .../profile/components/profile-header.tsx | 32 ++-- .../components/profile-security-card.tsx | 49 ++--- .../components/profile-settings-card.tsx | 43 ++--- .../components/sidebar-modules-card.tsx | 12 +- .../components/tabs/account-bindings-tab.tsx | 22 +-- .../components/tabs/notification-tab.tsx | 54 +++--- .../profile/components/two-fa-card.tsx | 16 +- web/default/src/features/profile/index.tsx | 15 +- web/default/src/features/profile/types.ts | 2 + .../components/redemptions-mutate-drawer.tsx | 10 +- .../components/redemptions-table.tsx | 4 +- .../dialogs/subscription-purchase-dialog.tsx | 10 +- .../subscriptions-mutate-drawer.tsx | 18 +- .../components/subscriptions-table.tsx | 2 +- .../columns/common-logs-columns.tsx | 2 +- .../components/common-logs-filter-bar.tsx | 14 +- .../components/dialogs/details-dialog.tsx | 41 +++-- .../dialogs/usage-logs-filter-dialog.tsx | 12 +- .../components/task-logs-filter-bar.tsx | 6 +- .../components/usage-logs-table.tsx | 8 +- .../users/components/users-mutate-drawer.tsx | 8 +- .../features/users/components/users-table.tsx | 4 +- .../components/affiliate-rewards-card.tsx | 167 ++++++------------ .../components/creem-products-section.tsx | 6 +- .../dialogs/billing-history-dialog.tsx | 26 +-- .../dialogs/creem-confirm-dialog.tsx | 6 +- .../dialogs/payment-confirm-dialog.tsx | 6 +- .../components/dialogs/transfer-dialog.tsx | 6 +- .../wallet/components/recharge-form-card.tsx | 93 +++++----- .../components/subscription-plans-card.tsx | 66 ++++--- .../wallet/components/wallet-stats-card.tsx | 10 +- web/default/src/features/wallet/index.tsx | 41 +++-- web/default/src/i18n/locales/en.json | 6 + web/default/src/i18n/locales/fr.json | 6 + web/default/src/i18n/locales/ja.json | 6 + web/default/src/i18n/locales/ru.json | 6 + web/default/src/i18n/locales/vi.json | 6 + web/default/src/i18n/locales/zh.json | 6 + web/default/src/lib/time.ts | 14 ++ web/default/src/stores/auth-store.ts | 2 +- 84 files changed, 1174 insertions(+), 731 deletions(-) create mode 100644 web/default/src/components/ui/titled-card.tsx create mode 100644 web/default/src/features/profile/components/language-preferences-card.tsx diff --git a/web/default/src/components/data-table/mobile-card-list.tsx b/web/default/src/components/data-table/mobile-card-list.tsx index feefe183..1ec364c3 100644 --- a/web/default/src/components/data-table/mobile-card-list.tsx +++ b/web/default/src/components/data-table/mobile-card-list.tsx @@ -62,7 +62,7 @@ function ListSkeleton() { -
+
@@ -136,9 +136,9 @@ function CompactRow({ row }: { row: Row }) { )}
- {/* Row 2: Key fields side by side */} + {/* Row 2: Key fields wrap into compact columns instead of squeezing */} {fieldCells.length > 0 && ( -
+
{fieldCells.map((cell) => { const label = getCellLabel(cell) return ( @@ -260,7 +260,7 @@ export function MobileCardList(props: MobileCardListProps) { if (!rows || rows.length === 0) { return ( -
+
diff --git a/web/default/src/components/data-table/pagination.tsx b/web/default/src/components/data-table/pagination.tsx index e140be97..b2f3b049 100644 --- a/web/default/src/components/data-table/pagination.tsx +++ b/web/default/src/components/data-table/pagination.tsx @@ -32,12 +32,12 @@ export function DataTablePagination({
-
-
+
+
{t('Page {{current}} of {{total}}', { current: currentPage, total: totalPages, @@ -50,7 +50,7 @@ export function DataTablePagination({ table.setPageSize(Number(value)) }} > - + @@ -74,7 +74,7 @@ export function DataTablePagination({ total: totalPages, })}
-
+
diff --git a/web/default/src/components/layout/components/section-page-layout.tsx b/web/default/src/components/layout/components/section-page-layout.tsx index 5c7120c6..ca4c6eb8 100644 --- a/web/default/src/components/layout/components/section-page-layout.tsx +++ b/web/default/src/components/layout/components/section-page-layout.tsx @@ -70,15 +70,15 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
-
- {breadcrumb != null &&
{breadcrumb}
} -
+
+ {breadcrumb != null &&
{breadcrumb}
} +
-

+

{title}

{description != null && ( -

+

{description}

)} @@ -91,11 +91,13 @@ export function SectionPageLayout(props: SectionPageLayoutProps) {
-
{content}
+
+ {content} +
diff --git a/web/default/src/components/ui/titled-card.tsx b/web/default/src/components/ui/titled-card.tsx new file mode 100644 index 00000000..f7d1ecf7 --- /dev/null +++ b/web/default/src/components/ui/titled-card.tsx @@ -0,0 +1,81 @@ +import type { ReactNode } from 'react' +import { cn } from '@/lib/utils' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from './card' + +type TitledCardProps = { + title: ReactNode + description?: ReactNode + icon?: ReactNode + action?: ReactNode + children?: ReactNode + className?: string + headerClassName?: string + contentClassName?: string + iconClassName?: string + titleClassName?: string + descriptionClassName?: string +} + +export function TitledCard({ + title, + description, + icon, + action, + children, + className, + headerClassName, + contentClassName, + iconClassName, + titleClassName, + descriptionClassName, +}: TitledCardProps) { + return ( + + +
+
+ {icon != null && ( +
+ {icon} +
+ )} +
+ + {title} + + {description != null && ( + + {description} + + )} +
+
+ {action != null &&
{action}
} +
+
+ + {children} + +
+ ) +} diff --git a/web/default/src/features/auth/hooks/use-auth-redirect.ts b/web/default/src/features/auth/hooks/use-auth-redirect.ts index f0371cd9..34721ae4 100644 --- a/web/default/src/features/auth/hooks/use-auth-redirect.ts +++ b/web/default/src/features/auth/hooks/use-auth-redirect.ts @@ -5,6 +5,24 @@ import { getSelf } from '@/lib/api' import type { User } from '@/features/users/types' import { saveUserId } from '../lib/storage' +function getSavedLanguage(user: User): string | undefined { + const userData = user as Record + if (typeof userData.language === 'string') { + return userData.language + } + + if (typeof userData.setting !== 'string') { + return undefined + } + + try { + const setting = JSON.parse(userData.setting) as { language?: unknown } + return typeof setting.language === 'string' ? setting.language : undefined + } catch { + return undefined + } +} + /** * Hook for handling authentication redirects and user data management */ @@ -39,9 +57,7 @@ export function useAuthRedirect() { } // Restore saved language preference - const savedLang = (user as Record).language as - | string - | undefined + const savedLang = getSavedLanguage(user) if (savedLang && savedLang !== i18n.language) { i18n.changeLanguage(savedLang) } diff --git a/web/default/src/features/channels/components/channels-table.tsx b/web/default/src/features/channels/components/channels-table.tsx index 7bb2f36c..86a5aaf4 100644 --- a/web/default/src/features/channels/components/channels-table.tsx +++ b/web/default/src/features/channels/components/channels-table.tsx @@ -87,7 +87,10 @@ export function ChannelsTable() { } = useTableUrlState({ search: route.useSearch(), navigate: route.useNavigate(), - pagination: { defaultPage: 1, defaultPageSize: DEFAULT_PAGE_SIZE }, + pagination: { + defaultPage: 1, + defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE, + }, globalFilter: { enabled: true, key: 'filter' }, columnFilters: [ { columnId: 'status', searchKey: 'status', type: 'array' }, @@ -329,7 +332,7 @@ export function ChannelsTable() { return ( <> -
+
- - + + {getLobeIcon(`${getChannelTypeIcon(currentType)}.Color`, 22)} @@ -1110,10 +1110,10 @@ export function ChannelMutateDrawer({
{/* ── Basic Information ── */} -
+
} @@ -3276,7 +3276,7 @@ export function ChannelMutateDrawer({ - +
-
+
{themeReady && spec && ( { const days = props.preferences.defaultTimeRangeDays - const { start, end } = getNormalizedDateRange(days) + const { start, end } = getRollingDateRange(days) setFilters({ ...buildDefaultDashboardFilters(props.preferences), start_timestamp: start, @@ -109,7 +109,7 @@ export function ModelsFilter(props: ModelsFilterProps) { } const handleQuickRange = (days: number) => { - const { start, end } = getNormalizedDateRange(days) + const { start, end } = getRollingDateRange(days) setFilters((prev) => ({ ...prev, @@ -127,7 +127,7 @@ export function ModelsFilter(props: ModelsFilterProps) { {t('Filter')} - + {t('Filter Dashboard Models')} @@ -137,15 +137,15 @@ export function ModelsFilter(props: ModelsFilterProps) { - -
+ +
{/* Quick time range selection */}
-
+
{TIME_RANGE_PRESETS.map((range) => ( diff --git a/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx b/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx index b8138aa5..8d19c292 100644 --- a/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx +++ b/web/default/src/features/keys/components/api-keys-mutate-drawer.tsx @@ -79,18 +79,18 @@ function ApiKeyFormSection(props: ApiKeyFormSectionProps) { return (
-
-
- +
+
+

{props.title}

-

+

{props.description}

-
{props.children}
+
{props.children}
) } @@ -254,13 +254,13 @@ export function ApiKeysMutateDrawer({ > - - + + {isUpdate ? t('Update API Key') : t('Create API Key')} - + {isUpdate ? t('Update the API key by providing necessary info.') : t('Add a new API key by providing necessary info.')}{' '} @@ -271,7 +271,7 @@ export function ApiKeysMutateDrawer({
( - +
{t('Cross-group retry')} - + {t( 'When enabled, if channels in the current group fail, it will try channels in the next group in order.' )} @@ -353,7 +353,7 @@ export function ApiKeysMutateDrawer({ value={field.value} onChange={field.onChange} placeholder={t('Never expires')} - className='min-w-0' + className='min-w-0 [&_input[type=time]]:w-24 sm:[&_input[type=time]]:w-32' />
@@ -361,7 +361,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(0, 0, 0)} > {t('Never')} @@ -370,7 +370,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(1, 0, 0)} > {t('1 Month')} @@ -379,7 +379,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(0, 1, 0)} > {t('1 Day')} @@ -388,7 +388,7 @@ export function ApiKeysMutateDrawer({ type='button' variant='outline' size='sm' - className='px-3' + className='px-2 text-xs sm:px-3 sm:text-sm' onClick={() => handleSetExpiry(0, 0, 1)} > {t('1 Hour')} @@ -470,7 +470,7 @@ export function ApiKeysMutateDrawer({ control={form.control} name='unlimited_quota' render={({ field }) => ( - +
{t('Unlimited Quota')} @@ -495,10 +495,10 @@ export function ApiKeysMutateDrawer({ -
+
- + - + - diff --git a/web/default/src/features/keys/components/api-keys-table.tsx b/web/default/src/features/keys/components/api-keys-table.tsx index 6c55e34f..51c9d12a 100644 --- a/web/default/src/features/keys/components/api-keys-table.tsx +++ b/web/default/src/features/keys/components/api-keys-table.tsx @@ -16,7 +16,9 @@ import { import { useMediaQuery } from '@/hooks' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' +import { formatQuota } from '@/lib/format' import { cn } from '@/lib/utils' +import { Database } from 'lucide-react' import { useTableUrlState } from '@/hooks/use-table-url-state' import { Table, @@ -33,16 +35,31 @@ import { DataTableToolbar, TableSkeleton, TableEmpty, - MobileCardList, } from '@/components/data-table' +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/empty' import { PageFooterPortal } from '@/components/layout' +import { Skeleton } from '@/components/ui/skeleton' +import { StatusBadge } from '@/components/status-badge' import { getApiKeys, searchApiKeys } from '../api' -import { API_KEY_STATUS, API_KEY_STATUS_OPTIONS, ERROR_MESSAGES } from '../constants' +import { + API_KEY_STATUS, + API_KEY_STATUS_OPTIONS, + API_KEY_STATUSES, + ERROR_MESSAGES, +} from '../constants' import { type ApiKey } from '../types' +import { ApiKeyCell } from './api-keys-cells' import { useApiKeysColumns } from './api-keys-columns' import { ApiKeysPrimaryButtons } from './api-keys-primary-buttons' import { useApiKeys } from './api-keys-provider' import { DataTableBulkActions } from './data-table-bulk-actions' +import { DataTableRowActions } from './data-table-row-actions' const route = getRouteApi('/_authenticated/keys/') @@ -50,6 +67,123 @@ function isDisabledApiKeyRow(apiKey: ApiKey) { return apiKey.status !== API_KEY_STATUS.ENABLED } +function ApiKeysMobileSkeleton() { + return ( +
+ {Array.from({ length: 5 }).map((_, index) => ( +
+
+ + +
+
+ + +
+ +
+ ))} +
+ ) +} + +function ApiKeysMobileList({ + table, + isLoading, +}: { + table: ReturnType> + isLoading: boolean +}) { + const { t } = useTranslation() + const rows = table.getRowModel().rows + + if (isLoading) return + + if (!rows.length) { + return ( +
+ + + + + + {t('No API Keys Found')} + + {t( + 'No API keys available. Create your first API key to get started.' + )} + + + +
+ ) + } + + return ( +
+ {rows.map((row) => { + const apiKey = row.original + const statusConfig = API_KEY_STATUSES[apiKey.status] + const total = apiKey.used_quota + apiKey.remain_quota + + return ( +
+
+
+
+ {apiKey.name} +
+
+ {t('API Key')} +
+
+ {statusConfig && ( + + )} +
+ +
+
+ +
+ +
+ +
+ {t('Quota')} + {apiKey.unlimited_quota ? ( + {t('Unlimited')} + ) : ( + + {formatQuota(apiKey.remain_quota)} + + {' / '} + {formatQuota(total)} + + + )} +
+
+ ) + })} +
+ ) +} + export function ApiKeysTable() { const { t } = useTranslation() const { refreshTrigger } = useApiKeys() @@ -166,7 +300,7 @@ export function ApiKeysTable() { return ( <> -
+
@@ -184,18 +318,9 @@ export function ApiKeysTable() {
{isMobile ? ( - - isDisabledApiKeyRow(row.original) - ? DISABLED_ROW_MOBILE - : undefined - } /> ) : (
-
+
- + {title} @@ -205,14 +205,14 @@ export function UpdateConfigDialog({
) : ( -
+
-
+
-
+
-
+
-
+
- +
- diff --git a/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx b/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx index e76055bb..14e44da0 100644 --- a/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx +++ b/web/default/src/features/models/components/dialogs/view-logs-dialog.tsx @@ -124,7 +124,7 @@ export function ViewLogsDialog({ return ( - + @@ -132,11 +132,11 @@ export function ViewLogsDialog({ -
+
{t('Deployment ID')}: {deploymentId}
-
+
-
+
{t('Auto refresh')}
-
+
{t('Container')} @@ -234,7 +234,7 @@ export function ViewLogsDialog({
{ const target = e.target as HTMLDivElement const isAtBottom = diff --git a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx index ffbe8914..996de746 100644 --- a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx +++ b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx @@ -601,8 +601,8 @@ export function ModelMutateDrawer({ return ( - - + + {isEditing ? t('Edit Model') : t('Create Model')} @@ -621,7 +621,7 @@ export function ModelMutateDrawer({ onSubmit={form.handleSubmit( onSubmit as Parameters[0] )} - className='flex-1 space-y-6 overflow-y-auto px-4' + className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4' > {/* Basic Information */}
@@ -1232,7 +1232,7 @@ export function ModelMutateDrawer({ - +