mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
improvement: usage-indicator UI (#1948)
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user