feat(ui): improve mobile responsive layouts
This commit is contained in:
+4
-4
@@ -77,7 +77,7 @@ export function ConsumptionDistributionChart(
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='flex w-full flex-col gap-3 border-b px-4 py-3 sm:px-5 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex w-full flex-col gap-1.5 border-b px-3 py-2 sm:gap-3 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<WalletCards className='text-muted-foreground/60 size-4' />
|
||||
<div className='text-sm font-semibold'>
|
||||
@@ -88,7 +88,7 @@ export function ConsumptionDistributionChart(
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
|
||||
<div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-md border p-0.5 sm:h-8 sm:w-auto'>
|
||||
{CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
|
||||
const Icon = CHART_TYPE_ICONS[item.value]
|
||||
return (
|
||||
@@ -96,7 +96,7 @@ export function ConsumptionDistributionChart(
|
||||
key={item.value}
|
||||
type='button'
|
||||
onClick={() => setChartType(item.value)}
|
||||
className={`inline-flex items-center gap-1.5 rounded-[5px] px-3 text-xs font-medium transition-colors ${
|
||||
className={`inline-flex shrink-0 items-center gap-1.5 rounded-[5px] px-3 text-xs font-medium transition-colors ${
|
||||
chartType === item.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
@@ -110,7 +110,7 @@ export function ConsumptionDistributionChart(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='h-96 p-2'>
|
||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||
{themeReady && spec && (
|
||||
<VChart
|
||||
key={`${chartType}-${resolvedTheme}`}
|
||||
|
||||
@@ -95,10 +95,13 @@ export function LogStatCards(props: LogStatCardsProps) {
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-3 lg:grid-cols-5'>
|
||||
{items.map((it) => {
|
||||
{items.map((it, idx) => {
|
||||
const Icon = it.icon
|
||||
return (
|
||||
<div key={it.title} className='px-4 py-3.5 sm:px-5 sm:py-4'>
|
||||
<div
|
||||
key={it.title}
|
||||
className={`px-3 py-2.5 sm:px-5 sm:py-4 ${idx === items.length - 1 && items.length % 2 !== 0 ? 'col-span-2 sm:col-span-1' : ''}`}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
|
||||
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
|
||||
@@ -113,7 +116,7 @@ export function LogStatCards(props: LogStatCardsProps) {
|
||||
</div>
|
||||
) : error ? (
|
||||
<>
|
||||
<div className='text-muted-foreground mt-2 font-mono text-2xl font-bold tracking-tight tabular-nums'>
|
||||
<div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
||||
--
|
||||
</div>
|
||||
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
|
||||
@@ -122,7 +125,7 @@ export function LogStatCards(props: LogStatCardsProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className='text-foreground mt-2 font-mono text-2xl font-bold tracking-tight tabular-nums'>
|
||||
<div className='text-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
|
||||
{it.value}
|
||||
</div>
|
||||
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='flex w-full flex-col gap-3 border-b px-4 py-3 sm:px-5 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex w-full flex-col gap-1.5 border-b px-3 py-2 sm:gap-3 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<PieChartIcon className='text-muted-foreground/60 size-4' />
|
||||
<div className='text-sm font-semibold'>
|
||||
@@ -89,13 +89,13 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
|
||||
<div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-md border p-0.5 sm:h-8 sm:w-auto'>
|
||||
{MODEL_ANALYTICS_CHART_OPTIONS.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
type='button'
|
||||
onClick={() => setActiveTab(tab.value)}
|
||||
className={`rounded-[5px] px-3 text-xs font-medium transition-colors ${
|
||||
className={`shrink-0 rounded-[5px] px-3 text-xs font-medium transition-colors ${
|
||||
activeTab === tab.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
@@ -107,7 +107,7 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='h-96 p-2'>
|
||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||
{themeReady && spec && (
|
||||
<VChart
|
||||
key={`${activeTab}-${resolvedTheme}`}
|
||||
|
||||
+9
-9
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Filter, RotateCcw, Calendar, Search } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { getNormalizedDateRange, type TimeGranularity } from '@/lib/time'
|
||||
import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -88,7 +88,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
||||
|
||||
const handleReset = () => {
|
||||
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')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col sm:max-w-lg'>
|
||||
<DialogContent className='flex max-h-[calc(100dvh-2rem)] flex-col max-sm:h-dvh max-sm:w-screen max-sm:max-w-none max-sm:rounded-none max-sm:p-4 sm:max-w-lg'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Filter Dashboard Models')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -137,15 +137,15 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className='flex-1 pr-4'>
|
||||
<div className='grid gap-4 py-4'>
|
||||
<ScrollArea className='flex-1 pr-3 sm:pr-4'>
|
||||
<div className='grid gap-3 py-3 sm:gap-4 sm:py-4'>
|
||||
{/* Quick time range selection */}
|
||||
<div className='grid gap-2'>
|
||||
<Label className='flex items-center gap-2'>
|
||||
<Calendar className='h-4 w-4' />
|
||||
{t('Quick Range')}
|
||||
</Label>
|
||||
<div className='flex gap-2'>
|
||||
<div className='grid grid-cols-2 gap-2 sm:flex'>
|
||||
{TIME_RANGE_PRESETS.map((range) => (
|
||||
<Button
|
||||
key={range.days}
|
||||
@@ -170,7 +170,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
||||
<SectionDivider label={t('Custom Time Range')} />
|
||||
|
||||
{/* Custom time range */}
|
||||
<div className='grid gap-4'>
|
||||
<div className='grid gap-3 sm:gap-4'>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='start_timestamp'>{t('Start Time')}</Label>
|
||||
<DateTimePicker
|
||||
@@ -236,7 +236,7 @@ export function ModelsFilter(props: ModelsFilterProps) {
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogFooter className='grid grid-cols-2 gap-2 sm:flex'>
|
||||
<Button onClick={handleReset} variant='outline' type='button'>
|
||||
<RotateCcw className='mr-2 h-4 w-4' />
|
||||
{t('Reset')}
|
||||
|
||||
+4
-4
@@ -47,10 +47,10 @@ export function AnnouncementsPanel() {
|
||||
loading={loading}
|
||||
empty={!list.length}
|
||||
emptyMessage={t('No announcements at this time')}
|
||||
height='h-64'
|
||||
height='h-56 sm:h-64'
|
||||
>
|
||||
<ScrollArea className='h-64'>
|
||||
<div className='-mx-4 sm:-mx-5'>
|
||||
<ScrollArea className='h-56 sm:h-64'>
|
||||
<div className='-mx-3 sm:-mx-5'>
|
||||
{list.map((item: AnnouncementItem, idx: number) => {
|
||||
const key = item.id ?? `announcement-${idx}`
|
||||
return (
|
||||
@@ -59,7 +59,7 @@ export function AnnouncementsPanel() {
|
||||
type='button'
|
||||
onClick={() => handleAnnouncementClick(item)}
|
||||
className={cn(
|
||||
'group hover:bg-muted/40 w-full px-4 py-3.5 text-left transition-colors sm:px-5',
|
||||
'group hover:bg-muted/40 w-full px-3 py-3 text-left transition-colors sm:px-5 sm:py-3.5',
|
||||
idx < list.length - 1 && 'border-border/60 border-b'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -23,8 +23,8 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
const status = props.status
|
||||
|
||||
return (
|
||||
<div className='group hover:bg-muted/40 flex items-center justify-between gap-3 px-4 py-3 transition-colors sm:px-5'>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-3'>
|
||||
<div className='group hover:bg-muted/40 flex items-center justify-between gap-2 px-3 py-2.5 transition-colors sm:gap-3 sm:px-5 sm:py-3'>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2 sm:gap-3'>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block size-2 shrink-0 rounded-full',
|
||||
@@ -91,7 +91,7 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => openExternalSpeedTest(item.url)}
|
||||
className='size-7 p-0'
|
||||
className='hidden size-7 p-0 sm:inline-flex'
|
||||
title={t('External Speed Test')}
|
||||
>
|
||||
<Gauge className='size-3.5' />
|
||||
@@ -111,7 +111,7 @@ export function ApiInfoItemComponent(props: ApiInfoItemProps) {
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
asChild
|
||||
className='size-7 p-0'
|
||||
className='hidden size-7 p-0 sm:inline-flex'
|
||||
title={t('Open in New Tab')}
|
||||
>
|
||||
<a href={item.url} target='_blank' rel='noreferrer'>
|
||||
|
||||
@@ -37,10 +37,10 @@ export function ApiInfoPanel() {
|
||||
loading={loading}
|
||||
empty={!list.length}
|
||||
emptyMessage={t('No API routes configured')}
|
||||
height='h-64'
|
||||
height='h-56 sm:h-64'
|
||||
>
|
||||
<ScrollArea className='h-64'>
|
||||
<div className='-mx-4 sm:-mx-5'>
|
||||
<ScrollArea className='h-56 sm:h-64'>
|
||||
<div className='-mx-3 sm:-mx-5'>
|
||||
{list.map((item: ApiInfoItem, idx: number) => (
|
||||
<div
|
||||
key={item.url}
|
||||
|
||||
@@ -27,9 +27,9 @@ export function FAQPanel() {
|
||||
loading={loading}
|
||||
empty={!list.length}
|
||||
emptyMessage={t('No FAQ entries available')}
|
||||
height='h-80'
|
||||
height='h-64 sm:h-80'
|
||||
>
|
||||
<ScrollArea className='h-80'>
|
||||
<ScrollArea className='h-64 sm:h-80'>
|
||||
<Accordion type='single' collapsible className='w-full'>
|
||||
{list.map((item: FAQItem, idx: number) => {
|
||||
const key = item.id ?? `faq-${idx}`
|
||||
|
||||
@@ -53,14 +53,9 @@ export function SummaryCards() {
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<StaggerContainer className='grid sm:grid-cols-2 lg:grid-cols-3'>
|
||||
{items.map((it, idx) => (
|
||||
<StaggerItem
|
||||
key={it.title}
|
||||
className={`px-4 sm:px-5 ${
|
||||
idx > 0 ? 'border-t sm:border-t-0 sm:border-l' : ''
|
||||
}`}
|
||||
>
|
||||
<StaggerContainer className='divide-border/60 grid grid-cols-3 divide-x'>
|
||||
{items.map((it) => (
|
||||
<StaggerItem key={it.title} className='px-3 py-3 sm:px-5 sm:py-4'>
|
||||
<StatCard
|
||||
title={it.title}
|
||||
value={it.value}
|
||||
@@ -72,7 +67,7 @@ export function SummaryCards() {
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='h-6 gap-1 px-2 text-xs'
|
||||
className='hidden h-6 gap-1 px-2 text-xs sm:inline-flex'
|
||||
asChild
|
||||
>
|
||||
<Link to='/wallet'>
|
||||
|
||||
@@ -84,7 +84,7 @@ export function UptimePanel() {
|
||||
loading={loading}
|
||||
empty={!groups.length}
|
||||
emptyMessage={t('No uptime monitoring configured')}
|
||||
height='h-80'
|
||||
height='h-64 sm:h-80'
|
||||
headerActions={
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -100,11 +100,11 @@ export function UptimePanel() {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ScrollArea className='h-80'>
|
||||
<div className='-mx-4 space-y-0 sm:-mx-5'>
|
||||
<ScrollArea className='h-64 sm:h-80'>
|
||||
<div className='-mx-3 space-y-0 sm:-mx-5'>
|
||||
{groups.map((group, groupIdx) => (
|
||||
<div key={group.categoryName}>
|
||||
<div className='bg-muted/30 border-border/60 border-b px-4 py-2 sm:px-5'>
|
||||
<div className='bg-muted/30 border-border/60 border-b px-3 py-2 sm:px-5'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<h4 className='text-muted-foreground text-xs font-semibold tracking-wider uppercase'>
|
||||
{group.categoryName}
|
||||
@@ -120,7 +120,7 @@ export function UptimePanel() {
|
||||
<div
|
||||
key={monitor.name}
|
||||
className={cn(
|
||||
'hover:bg-muted/40 flex items-center justify-between px-4 py-2.5 transition-colors sm:px-5',
|
||||
'hover:bg-muted/40 flex items-center justify-between gap-2 px-3 py-2 transition-colors sm:px-5 sm:py-2.5',
|
||||
monitorIdx < (group.monitors?.length || 0) - 1 &&
|
||||
'border-border/40 border-b',
|
||||
groupIdx < groups.length - 1 &&
|
||||
|
||||
@@ -20,10 +20,10 @@ export function PanelWrapper(props: PanelWrapperProps) {
|
||||
if (props.loading) {
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='border-b px-4 py-3 sm:px-5'>
|
||||
<div className='border-b px-3 py-2.5 sm:px-5 sm:py-3'>
|
||||
<div className='text-sm font-semibold'>{props.title}</div>
|
||||
</div>
|
||||
<div className='p-4 sm:p-5'>
|
||||
<div className='p-3 sm:p-5'>
|
||||
<Skeleton className={`w-full ${height}`} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
|
||||
if (props.empty) {
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='border-b px-4 py-3 sm:px-5'>
|
||||
<div className='border-b px-3 py-2.5 sm:px-5 sm:py-3'>
|
||||
<div className='text-sm font-semibold'>{props.title}</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -47,9 +47,9 @@ export function PanelWrapper(props: PanelWrapperProps) {
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border'>
|
||||
<div className='border-b px-4 py-3 sm:px-5'>
|
||||
<div className='border-b px-3 py-2.5 sm:px-5 sm:py-3'>
|
||||
{props.headerActions ? (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<div className='text-sm font-semibold'>{props.title}</div>
|
||||
{props.headerActions}
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export function PanelWrapper(props: PanelWrapperProps) {
|
||||
<div className='text-sm font-semibold'>{props.title}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='p-4 sm:p-5'>{props.children}</div>
|
||||
<div className='p-3 sm:p-5'>{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+12
-10
@@ -15,13 +15,15 @@ export function StatCard(props: StatCardProps) {
|
||||
const Icon = props.icon
|
||||
|
||||
return (
|
||||
<div className='group flex flex-col gap-1.5 py-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wider uppercase'>
|
||||
<Icon className='text-muted-foreground/60 size-3.5' />
|
||||
{props.title}
|
||||
<div className='group flex flex-col gap-1'>
|
||||
<div className='flex items-start justify-between gap-1'>
|
||||
<div className='text-muted-foreground flex items-center gap-1.5 text-xs font-medium tracking-wider uppercase sm:gap-2'>
|
||||
<Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
|
||||
<span className='line-clamp-2 leading-snug'>{props.title}</span>
|
||||
</div>
|
||||
{props.action}
|
||||
{props.action && (
|
||||
<div className='shrink-0'>{props.action}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.loading ? (
|
||||
@@ -31,19 +33,19 @@ export function StatCard(props: StatCardProps) {
|
||||
</div>
|
||||
) : props.error ? (
|
||||
<>
|
||||
<div className='text-muted-foreground font-mono text-2xl font-bold tracking-tight tabular-nums'>
|
||||
<div className='text-muted-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
|
||||
--
|
||||
</div>
|
||||
<p className='text-muted-foreground/60 text-xs'>
|
||||
<p className='text-muted-foreground/60 hidden text-xs md:block'>
|
||||
{props.description}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className='text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums'>
|
||||
<div className='text-foreground mt-0.5 font-mono text-base font-bold tracking-tight break-all tabular-nums sm:text-2xl'>
|
||||
{props.value}
|
||||
</div>
|
||||
<p className='text-muted-foreground/60 text-xs'>
|
||||
<p className='text-muted-foreground/60 hidden text-xs md:block'>
|
||||
{props.description}
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { VChart } from '@visactor/react-vchart'
|
||||
import { Users, Loader2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getNormalizedDateRange, type TimeGranularity } from '@/lib/time'
|
||||
import { getRollingDateRange, type TimeGranularity } from '@/lib/time'
|
||||
import { VCHART_OPTION } from '@/lib/vchart'
|
||||
import { useTheme } from '@/context/theme-provider'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
@@ -60,7 +60,7 @@ export function UserCharts() {
|
||||
const [topUserLimit, setTopUserLimit] = useState(10)
|
||||
const [timeRange, setTimeRange] = useState(() => {
|
||||
const days = getDefaultDays(timeGranularity)
|
||||
const { start, end } = getNormalizedDateRange(days)
|
||||
const { start, end } = getRollingDateRange(days)
|
||||
return {
|
||||
start_timestamp: Math.floor(start.getTime() / 1000),
|
||||
end_timestamp: Math.floor(end.getTime() / 1000),
|
||||
@@ -69,7 +69,7 @@ export function UserCharts() {
|
||||
|
||||
const handleRangeChange = useCallback((days: number) => {
|
||||
setSelectedRange(days)
|
||||
const { start, end } = getNormalizedDateRange(days)
|
||||
const { start, end } = getRollingDateRange(days)
|
||||
setTimeRange({
|
||||
start_timestamp: Math.floor(start.getTime() / 1000),
|
||||
end_timestamp: Math.floor(end.getTime() / 1000),
|
||||
@@ -123,10 +123,9 @@ export function UserCharts() {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
{/* Toolbar: time range presets + granularity */}
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<div className='flex items-center gap-1.5 rounded-md border p-0.5'>
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center gap-1.5 overflow-x-auto pb-1 sm:gap-2'>
|
||||
<div className='flex shrink-0 items-center gap-1.5 rounded-md border p-0.5'>
|
||||
{TIME_RANGE_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset.days}
|
||||
@@ -143,7 +142,7 @@ export function UserCharts() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-1.5 rounded-md border p-0.5'>
|
||||
<div className='flex shrink-0 items-center gap-1.5 rounded-md border p-0.5'>
|
||||
{TIME_GRANULARITY_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
@@ -162,7 +161,7 @@ export function UserCharts() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-1.5 rounded-md border p-0.5'>
|
||||
<div className='flex shrink-0 items-center gap-1.5 rounded-md border p-0.5'>
|
||||
<span className='text-muted-foreground px-2 text-xs font-medium'>
|
||||
{t('Top Users')}
|
||||
</span>
|
||||
@@ -187,7 +186,7 @@ export function UserCharts() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4'>
|
||||
<div className='grid gap-3'>
|
||||
{USER_CHARTS.map((chart) => {
|
||||
const spec = chartData[chart.specKey]
|
||||
|
||||
@@ -196,12 +195,12 @@ export function UserCharts() {
|
||||
key={chart.value}
|
||||
className='overflow-hidden rounded-lg border'
|
||||
>
|
||||
<div className='flex w-full items-center gap-2 border-b px-4 py-3 sm:px-5'>
|
||||
<div className='flex w-full items-center gap-2 border-b px-3 py-2 sm:px-5 sm:py-3'>
|
||||
<Users className='text-muted-foreground/60 size-4' />
|
||||
<div className='text-sm font-semibold'>{t(chart.labelKey)}</div>
|
||||
</div>
|
||||
|
||||
<div className='h-96 p-2'>
|
||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||
{isLoading ? (
|
||||
<Skeleton className='h-full w-full' />
|
||||
) : (
|
||||
|
||||
@@ -76,9 +76,7 @@ export function useSummaryCardsConfig(totals: {
|
||||
return [
|
||||
{
|
||||
key: 'balance',
|
||||
title: totals.currencyEnabled
|
||||
? `${t('Current Balance')} (${totals.currencyLabel})`
|
||||
: t('Current Balance'),
|
||||
title: t('Current Balance'),
|
||||
value: totals.remainDisplay,
|
||||
description: totals.currencyEnabled
|
||||
? `${t('Remaining quota')} (${totals.currencyLabel})`
|
||||
@@ -87,9 +85,7 @@ export function useSummaryCardsConfig(totals: {
|
||||
},
|
||||
{
|
||||
key: 'usage',
|
||||
title: totals.currencyEnabled
|
||||
? `${t('Historical Usage')} (${totals.currencyLabel})`
|
||||
: t('Historical Usage'),
|
||||
title: t('Historical Usage'),
|
||||
value: totals.usedDisplay,
|
||||
description: totals.currencyEnabled
|
||||
? `${t('Total consumed')} (${totals.currencyLabel})`
|
||||
|
||||
+4
-4
@@ -191,9 +191,9 @@ export function Dashboard() {
|
||||
{t(meta.descriptionKey)}
|
||||
</SectionPageLayout.Description>
|
||||
<SectionPageLayout.Content>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-3 sm:space-y-4'>
|
||||
{activeSection !== 'overview' && (
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-1.5 sm:gap-2'>
|
||||
{showSectionTabs ? (
|
||||
<Tabs value={activeSection} onValueChange={handleSectionChange}>
|
||||
<TabsList className='h-auto max-w-full flex-wrap justify-start'>
|
||||
@@ -208,7 +208,7 @@ export function Dashboard() {
|
||||
<div />
|
||||
)}
|
||||
{modelActions != null && (
|
||||
<div className='flex shrink-0 flex-wrap items-center gap-2'>
|
||||
<div className='flex shrink-0 flex-wrap items-center gap-1.5 sm:gap-2'>
|
||||
{modelActions}
|
||||
</div>
|
||||
)}
|
||||
@@ -217,7 +217,7 @@ export function Dashboard() {
|
||||
{activeSection === 'overview' && (
|
||||
<>
|
||||
<SummaryCards />
|
||||
<CardStaggerContainer className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
|
||||
<CardStaggerContainer className='grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-2'>
|
||||
<CardStaggerItem>
|
||||
<ApiInfoPanel />
|
||||
</CardStaggerItem>
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { TimeGranularity } from '@/lib/time'
|
||||
import { getNormalizedDateRange } from '@/lib/time'
|
||||
import { getRollingDateRange } from '@/lib/time'
|
||||
import {
|
||||
DASHBOARD_CHART_PREFERENCES_STORAGE_KEY,
|
||||
DEFAULT_DASHBOARD_CHART_PREFERENCES,
|
||||
@@ -128,7 +128,7 @@ export function getDefaultDays(granularity?: TimeGranularity): number {
|
||||
export function buildDefaultDashboardFilters(
|
||||
preferences: DashboardChartPreferences = getSavedChartPreferences()
|
||||
): DashboardFilters {
|
||||
const { start, end } = getNormalizedDateRange(preferences.defaultTimeRangeDays)
|
||||
const { start, end } = getRollingDateRange(preferences.defaultTimeRangeDays)
|
||||
return {
|
||||
...EMPTY_DASHBOARD_FILTERS,
|
||||
start_timestamp: start,
|
||||
|
||||
Reference in New Issue
Block a user