improvement: usage-indicator UI (#1948)

This commit is contained in:
Emir Karabeg
2025-11-12 14:09:18 -08:00
committed by GitHub
parent 9db969b1e0
commit 36bcd75832
2 changed files with 100 additions and 51 deletions

View File

@@ -1,22 +1,32 @@
'use client'
import { useEffect } from 'react'
import { Badge, Progress, Skeleton } from '@/components/ui'
import { useEffect, useMemo } from 'react'
import { Button } from '@/components/emcn'
import { Skeleton } from '@/components/ui'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { MIN_SIDEBAR_WIDTH, useSidebarStore } from '@/stores/sidebar/store'
import { useSubscriptionStore } from '@/stores/subscription/store'
// Constants for reusable styles
const GRADIENT_BADGE_STYLES =
'gradient-text h-[1.125rem] rounded-[6px] border-gradient-primary/20 bg-gradient-to-b from-gradient-primary via-gradient-secondary to-gradient-primary px-2 py-0 font-medium text-xs'
const GRADIENT_TEXT_STYLES =
'gradient-text bg-gradient-to-b from-gradient-primary via-gradient-secondary to-gradient-primary'
const CONTAINER_STYLES =
'pointer-events-auto flex-shrink-0 rounded-[10px] border bg-background px-3 py-2.5 shadow-xs cursor-pointer transition-colors hover:bg-muted/50'
const logger = createLogger('UsageIndicator')
// Plan name mapping
/**
* Minimum number of pills to display (at minimum sidebar width)
*/
const MIN_PILL_COUNT = 6
/**
* Maximum number of pills to display
*/
const MAX_PILL_COUNT = 8
/**
* Width increase (in pixels) required to add one additional pill
*/
const WIDTH_PER_PILL = 50
/**
* Plan name mapping
*/
const PLAN_NAMES = {
enterprise: 'Enterprise',
team: 'Team',
@@ -30,26 +40,40 @@ interface UsageIndicatorProps {
export function UsageIndicator({ onClick }: UsageIndicatorProps) {
const { getUsage, getSubscriptionStatus, isLoading } = useSubscriptionStore()
const sidebarWidth = useSidebarStore((state) => state.sidebarWidth)
useEffect(() => {
useSubscriptionStore.getState().loadData()
}, [])
/**
* Calculate pill count based on sidebar width
* Starts at MIN_PILL_COUNT at minimum width, adds 1 pill per WIDTH_PER_PILL increase
*/
const pillCount = useMemo(() => {
const widthDelta = sidebarWidth - MIN_SIDEBAR_WIDTH
const additionalPills = Math.floor(widthDelta / WIDTH_PER_PILL)
const calculatedCount = MIN_PILL_COUNT + additionalPills
return Math.max(MIN_PILL_COUNT, Math.min(MAX_PILL_COUNT, calculatedCount))
}, [sidebarWidth])
const usage = getUsage()
const subscription = getSubscriptionStatus()
if (isLoading) {
return (
<div className={CONTAINER_STYLES} onClick={() => onClick?.()}>
<div className='space-y-2'>
{/* Plan and usage info skeleton */}
<div className='flex items-center justify-between'>
<Skeleton className='h-5 w-12' />
<Skeleton className='h-4 w-20' />
</div>
<div className='flex flex-shrink-0 flex-col gap-[10px] border-t px-[13.5px] pt-[10px] pb-[8px] dark:border-[var(--border)]'>
{/* Top row skeleton */}
<div className='flex items-center justify-between'>
<Skeleton className='h-[16px] w-[120px] rounded-[4px]' />
<Skeleton className='h-[16px] w-[50px] rounded-[4px]' />
</div>
{/* Progress Bar skeleton */}
<Skeleton className='h-2 w-full' />
{/* Pills skeleton */}
<div className='flex items-center gap-[4px]'>
{Array.from({ length: pillCount }).map((_, i) => (
<Skeleton key={i} className='h-[6px] flex-1 rounded-[2px]' />
))}
</div>
</div>
)
@@ -67,7 +91,13 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
const billingStatus = useSubscriptionStore.getState().getBillingStatus()
const isBlocked = billingStatus === 'blocked'
const badgeText = isBlocked ? 'Payment Failed' : planType === 'free' ? 'Upgrade' : undefined
const showUpgradeButton = planType === 'free' || isBlocked
/**
* Calculate which pills should be filled based on usage percentage
*/
const filledPillsCount = Math.ceil((progressPercentage / 100) * pillCount)
const isAlmostOut = filledPillsCount === pillCount
const handleClick = () => {
try {
@@ -91,32 +121,56 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
}
return (
<div className={CONTAINER_STYLES} onClick={handleClick}>
<div className='space-y-2'>
{/* Plan and usage info */}
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<span
className={cn(
'font-medium text-sm',
planType === 'free' ? 'text-foreground' : GRADIENT_TEXT_STYLES
)}
>
{PLAN_NAMES[planType]}
</span>
{badgeText ? <Badge className={GRADIENT_BADGE_STYLES}>{badgeText}</Badge> : null}
<div className='flex flex-shrink-0 flex-col gap-[10px] border-t px-[13.5px] pt-[8px] pb-[8px] dark:border-[var(--border)]'>
{/* Top row */}
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[6px]'>
<span className='font-medium text-[#FFFFFF] text-[12px]'>{PLAN_NAMES[planType]}</span>
<div className='h-[14px] w-[1.5px] bg-[#4A4A4A]' />
<div className='flex items-center gap-[4px]'>
{isBlocked ? (
<>
<span className='font-medium text-[#B1B1B1] text-[12px]'>Over</span>
<span className='font-medium text-[#B1B1B1] text-[12px]'>limit</span>
</>
) : (
<>
<span className='font-medium text-[#B1B1B1] text-[12px] tabular-nums'>
${usage.current.toFixed(2)}
</span>
<span className='font-medium text-[#B1B1B1] text-[12px]'>/</span>
<span className='font-medium text-[#B1B1B1] text-[12px] tabular-nums'>
${usage.limit}
</span>
</>
)}
</div>
<span className='text-muted-foreground text-xs tabular-nums'>
{isBlocked ? 'Payment required' : `$${usage.current.toFixed(2)} / $${usage.limit}`}
</span>
</div>
{showUpgradeButton && (
<Button
variant='ghost'
className='!h-auto !px-1 !py-0 -mx-1 mt-[-2px] text-[#D4D4D4]'
onClick={handleClick}
>
Upgrade
</Button>
)}
</div>
{/* Progress Bar */}
<Progress
value={isBlocked ? 100 : progressPercentage}
className='h-2'
indicatorClassName='bg-black dark:bg-white'
/>
{/* Pills row */}
<div className='flex items-center gap-[4px]'>
{Array.from({ length: pillCount }).map((_, i) => {
const isFilled = i < filledPillsCount
return (
<div
key={i}
className='h-[6px] flex-1 rounded-[2px]'
style={{
backgroundColor: isFilled ? (isAlmostOut ? '#ef4444' : '#34B5FF') : '#414141',
}}
/>
)
})}
</div>
</div>
)

View File

@@ -33,7 +33,6 @@ const logger = createLogger('SidebarNew')
// Feature flag: Billing usage indicator visibility (matches legacy sidebar behavior)
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
// const isBillingEnabled = true
/**
* Sidebar component with resizable width that persists across page refreshes.
@@ -610,11 +609,7 @@ export function SidebarNew() {
</div>
{/* Usage Indicator */}
{isBillingEnabled && (
<div className='flex flex-shrink-0 flex-col gap-[2px] border-t px-[7.75px] pt-[8px] pb-[8px] dark:border-[var(--border)]'>
<UsageIndicator />
</div>
)}
{isBillingEnabled && <UsageIndicator />}
{/* Footer Navigation */}
<FooterNavigation />