improvement(ui): updated subscription and team settings modals to emcn (#2477)

This commit is contained in:
Waleed
2025-12-19 11:41:47 -08:00
committed by GitHub
parent 65787d7cc3
commit 6b15a50311
6 changed files with 235 additions and 244 deletions

View File

@@ -2,7 +2,15 @@
import { useEffect, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
import {
Button,
Label,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from '@/components/emcn'
import { useSession, useSubscription } from '@/lib/auth/auth-client'
import { getSubscriptionStatus } from '@/lib/billing/client/utils'
import { cn } from '@/lib/core/utils/cn'
@@ -68,7 +76,6 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
if (subscriptionStatus.isTeam && activeOrgId) {
referenceId = activeOrgId
// Get subscription ID for team/enterprise
subscriptionId = subData?.data?.id
}
@@ -132,14 +139,12 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
referenceId = activeOrgId
subscriptionId = subData?.data?.id
} else {
// For personal subscriptions, use user ID and let better-auth find the subscription
referenceId = session.user.id
subscriptionId = undefined
}
logger.info('Restoring subscription', { referenceId, subscriptionId })
// Build restore params - only include subscriptionId if we have one (team/enterprise)
const restoreParams: any = { referenceId }
if (subscriptionId) {
restoreParams.subscriptionId = subscriptionId
@@ -150,7 +155,6 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
logger.info('Subscription restored successfully', result)
}
// Invalidate queries to refresh data
await queryClient.invalidateQueries({ queryKey: subscriptionKeys.user() })
if (activeOrgId) {
await queryClient.invalidateQueries({ queryKey: organizationKeys.detail(activeOrgId) })
@@ -175,10 +179,8 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
if (!date) return 'end of current billing period'
try {
// Ensure we have a valid Date object
const dateObj = date instanceof Date ? date : new Date(date)
// Check if the date is valid
if (Number.isNaN(dateObj.getTime())) {
return 'end of current billing period'
}
@@ -196,20 +198,17 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
const periodEndDate = getPeriodEndDate()
// Check if subscription is set to cancel at period end
const isCancelAtPeriodEnd = subscriptionData?.cancelAtPeriodEnd === true
return (
<>
<div className='flex items-center justify-between'>
<div>
<span className='font-medium text-[13px]'>
{isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'}
</span>
<div className='flex flex-col gap-[2px]'>
<Label>{isCancelAtPeriodEnd ? 'Restore Subscription' : 'Manage Subscription'}</Label>
{isCancelAtPeriodEnd && (
<p className='mt-1 text-[var(--text-muted)] text-xs'>
<span className='text-[12px] text-[var(--text-muted)]'>
You'll keep access until {formatDate(periodEndDate)}
</p>
</span>
)}
</div>
<Button
@@ -217,7 +216,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
onClick={() => setIsDialogOpen(true)}
disabled={isLoading}
className={cn(
'h-8 rounded-[8px] font-medium text-xs',
'h-8 rounded-[8px] text-[13px]',
error && 'border-[var(--text-error)] text-[var(--text-error)]'
)}
>
@@ -231,7 +230,7 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
{isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} Subscription
</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-muted)]'>
{isCancelAtPeriodEnd
? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?'
: `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate(
@@ -244,8 +243,8 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
{!isCancelAtPeriodEnd && (
<div className='mt-3'>
<div className='rounded-[8px] bg-[var(--surface-3)] p-3 text-sm'>
<ul className='space-y-1 text-[var(--text-muted)] text-xs'>
<div className='rounded-[8px] bg-[var(--surface-5)] p-3'>
<ul className='space-y-1 text-[12px] text-[var(--text-muted)]'>
<li>• Keep all features until {formatDate(periodEndDate)}</li>
<li>• No more charges</li>
<li>• Data preserved</li>

View File

@@ -4,7 +4,9 @@ import { useState } from 'react'
import {
Button,
Input,
Label,
Modal,
ModalBody,
ModalClose,
ModalContent,
ModalFooter,
@@ -90,7 +92,6 @@ export function CreditBalance({
const handleOpenChange = (open: boolean) => {
setIsOpen(open)
if (open) {
// Generate new requestId when modal opens - same ID used for entire session
setRequestId(crypto.randomUUID())
} else {
setAmount('')
@@ -102,72 +103,66 @@ export function CreditBalance({
return (
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<span className='text-muted-foreground text-sm'>Credit Balance</span>
<span className='font-medium text-sm'>{isLoading ? '...' : `$${balance.toFixed(2)}`}</span>
<div className='flex items-center gap-[8px]'>
<Label>Credit Balance</Label>
<span className='text-[13px] text-[var(--text-secondary)]'>
{isLoading ? '...' : `$${balance.toFixed(2)}`}
</span>
</div>
{canPurchase && (
<Modal open={isOpen} onOpenChange={handleOpenChange}>
<ModalTrigger asChild>
<Button variant='outline'>Add Credits</Button>
<Button variant='outline' className='h-8 rounded-[8px] text-[13px]'>
Add Credits
</Button>
</ModalTrigger>
<ModalContent>
<ModalContent size='sm'>
<ModalHeader>Add Credits</ModalHeader>
<div className='px-4'>
<p className='text-[13px] text-[var(--text-secondary)]'>
Credits are used before overage charges. Min $10, max $1,000.
</p>
</div>
{success ? (
<div className='py-4 text-center'>
<p className='text-[14px] text-[var(--text-primary)]'>
<ModalBody>
{success ? (
<p className='text-center text-[13px] text-[var(--text-primary)]'>
Credits added successfully!
</p>
</div>
) : (
<div className='flex flex-col gap-3 py-2'>
<div className='flex flex-col gap-1'>
<label
htmlFor='credit-amount'
className='text-[12px] text-[var(--text-secondary)]'
>
Amount (USD)
</label>
<div className='relative'>
<span className='-translate-y-1/2 absolute top-1/2 left-3 text-[var(--text-secondary)]'>
$
</span>
<Input
id='credit-amount'
type='text'
inputMode='numeric'
value={amount}
onChange={(e) => handleAmountChange(e.target.value)}
placeholder='50'
className='pl-7'
disabled={isPurchasing}
/>
</div>
{error && <span className='text-[11px] text-red-500'>{error}</span>}
</div>
<div className='rounded-[4px] bg-[var(--surface-5)] p-2'>
<p className='text-[11px] text-[var(--text-tertiary)]'>
Credits are non-refundable and don't expire. They'll be applied automatically to
your {entityType === 'organization' ? 'team' : ''} usage.
) : (
<>
<p className='text-[12px] text-[var(--text-muted)]'>
Credits are used before overage charges. Min $10, max $1,000.
</p>
</div>
</div>
)}
<div className='mt-4 flex flex-col gap-[4px]'>
<Label htmlFor='credit-amount'>Amount (USD)</Label>
<div className='relative'>
<span className='-translate-y-1/2 absolute top-1/2 left-3 text-[13px] text-[var(--text-secondary)]'>
$
</span>
<Input
id='credit-amount'
type='text'
inputMode='numeric'
value={amount}
onChange={(e) => handleAmountChange(e.target.value)}
placeholder='50'
className='pl-7'
disabled={isPurchasing}
/>
</div>
{error && <span className='text-[12px] text-[var(--text-error)]'>{error}</span>}
</div>
<div className='mt-4 rounded-[6px] bg-[var(--surface-5)] p-3'>
<p className='text-[12px] text-[var(--text-muted)]'>
Credits are non-refundable and don't expire. They'll be applied automatically
to your {entityType === 'organization' ? 'team' : ''} usage.
</p>
</div>
</>
)}
</ModalBody>
{!success && (
<ModalFooter>
<ModalClose asChild>
<Button variant='ghost' disabled={isPurchasing}>
Cancel
</Button>
<Button disabled={isPurchasing}>Cancel</Button>
</ModalClose>
<Button
variant='primary'

View File

@@ -45,9 +45,9 @@ export function PlanCard({
if (typeof price === 'string') {
return (
<>
<span className='font-semibold text-xl'>{price}</span>
<span className='font-semibold text-[20px]'>{price}</span>
{priceSubtext && (
<span className='ml-1 text-[var(--text-muted)] text-xs'>{priceSubtext}</span>
<span className='ml-1 text-[12px] text-[var(--text-muted)]'>{priceSubtext}</span>
)}
</>
)
@@ -58,13 +58,13 @@ export function PlanCard({
const renderFeatures = () => {
if (isHorizontal) {
return (
<div className='mt-3 flex flex-wrap items-center gap-4'>
<div className='mt-3 flex flex-wrap items-center gap-3'>
{features.map((feature, index) => (
<div key={`${feature.text}-${index}`} className='flex items-center gap-2 text-xs'>
<feature.icon className='h-3 w-3 flex-shrink-0 text-[var(--text-muted)]' />
<span className='text-[var(--text-muted)]'>{feature.text}</span>
<div key={`${feature.text}-${index}`} className='flex items-center gap-2 text-[12px]'>
<feature.icon className='h-3 w-3 flex-shrink-0 text-[var(--text-secondary)]' />
<span className='text-[var(--text-secondary)]'>{feature.text}</span>
{index < features.length - 1 && (
<div className='ml-4 h-4 w-px bg-[var(--border)]' aria-hidden='true' />
<div className='ml-3 h-4 w-px bg-[var(--border)]' aria-hidden='true' />
)}
</div>
))}
@@ -75,12 +75,12 @@ export function PlanCard({
return (
<ul className='mb-4 flex-1 space-y-2'>
{features.map((feature, index) => (
<li key={`${feature.text}-${index}`} className='flex items-start gap-2 text-xs'>
<li key={`${feature.text}-${index}`} className='flex items-start gap-2 text-[12px]'>
<feature.icon
className='mt-0.5 h-3 w-3 flex-shrink-0 text-[var(--text-muted)]'
className='mt-0.5 h-3 w-3 flex-shrink-0 text-[var(--text-secondary)]'
aria-hidden='true'
/>
<span className='text-[var(--text-muted)]'>{feature.text}</span>
<span className='text-[var(--text-secondary)]'>{feature.text}</span>
</li>
))}
</ul>
@@ -91,24 +91,24 @@ export function PlanCard({
<article
className={cn(
'relative flex rounded-[8px] border p-4 transition-colors hover:border-[var(--border-hover)]',
isHorizontal ? 'flex-row items-center justify-between' : 'flex-col',
isHorizontal ? 'flex-row items-center justify-between gap-6' : 'flex-col',
className
)}
>
<header className={isHorizontal ? undefined : 'mb-4'}>
<h3 className='mb-2 font-semibold text-sm'>{name}</h3>
<header className={isHorizontal ? 'flex-1' : 'mb-4'}>
<h3 className='mb-2 font-semibold text-[14px]'>{name}</h3>
<div className='flex items-baseline'>{renderPrice()}</div>
{isHorizontal && renderFeatures()}
</header>
{!isHorizontal && renderFeatures()}
<div className={isHorizontal ? 'ml-auto' : undefined}>
<div className={isHorizontal ? 'flex-shrink-0' : undefined}>
<Button
onClick={onButtonClick}
className={cn(
'h-9 rounded-[8px] text-xs',
isHorizontal ? 'px-4' : 'w-full',
'h-9 rounded-[8px] text-[13px]',
isHorizontal ? 'min-w-[100px] px-6' : 'w-full',
isError && 'border-[var(--text-error)] text-[var(--text-error)]'
)}
variant='outline'

View File

@@ -1,17 +1,17 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Switch } from '@/components/emcn'
import { Skeleton } from '@/components/ui'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
Label,
Popover,
PopoverContent,
PopoverItem,
PopoverSection,
PopoverTrigger,
Switch,
} from '@/components/emcn'
import { Skeleton } from '@/components/ui'
import { useSession } from '@/lib/auth/auth-client'
import { useSubscriptionUpgrade } from '@/lib/billing/client/upgrade'
import { cn } from '@/lib/core/utils/cn'
@@ -270,7 +270,6 @@ export function Subscription() {
}
)
// UI state computed values
const showBadge = permissions.canEditUsageLimit && !permissions.showTeamMemberView
const badgeText = subscription.isFree ? 'Upgrade' : 'Increase Limit'
@@ -333,7 +332,7 @@ export function Subscription() {
<PlanCard
key='enterprise'
name='Enterprise'
price={<span className='font-semibold text-xl'>Custom</span>}
price={<span className='font-semibold text-[20px]'>Custom</span>}
priceSubtext={
layout === 'horizontal'
? 'Custom solutions tailored to your enterprise needs'
@@ -458,7 +457,7 @@ export function Subscription() {
{/* Enterprise Usage Limit Notice */}
{subscription.isEnterprise && (
<div className='text-center'>
<p className='text-[var(--text-muted)] text-xs'>
<p className='text-[12px] text-[var(--text-muted)]'>
Contact enterprise for support usage limit changes
</p>
</div>
@@ -467,7 +466,7 @@ export function Subscription() {
{/* Team Member Notice */}
{permissions.showTeamMemberView && (
<div className='text-center'>
<p className='text-[var(--text-muted)] text-xs'>
<p className='text-[12px] text-[var(--text-muted)]'>
Contact your team admin to increase limits
</p>
</div>
@@ -534,72 +533,78 @@ export function Subscription() {
{/* Next Billing Date */}
{subscription.isPaid && subscriptionData?.data?.periodEnd && (
<div className='flex items-center justify-between'>
<span className='font-medium text-[13px]'>Next Billing Date</span>
<span className='text-[13px] text-[var(--text-muted)]'>
<Label>Next Billing Date</Label>
<span className='text-[13px] text-[var(--text-secondary)]'>
{new Date(subscriptionData.data.periodEnd).toLocaleDateString()}
</span>
</div>
)}
{/* Billing usage notifications toggle */}
{/* Usage notifications */}
{subscription.isPaid && <BillingUsageNotificationsToggle />}
{/* Cancel Subscription */}
{permissions.canCancelSubscription && (
<div className='mt-[8px]'>
<CancelSubscription
subscription={{
plan: subscription.plan,
status: subscription.status,
isPaid: subscription.isPaid,
}}
subscriptionData={{
periodEnd: subscriptionData?.data?.periodEnd || null,
cancelAtPeriodEnd: subscriptionData?.data?.cancelAtPeriodEnd,
}}
/>
</div>
<CancelSubscription
subscription={{
plan: subscription.plan,
status: subscription.status,
isPaid: subscription.isPaid,
}}
subscriptionData={{
periodEnd: subscriptionData?.data?.periodEnd || null,
cancelAtPeriodEnd: subscriptionData?.data?.cancelAtPeriodEnd,
}}
/>
)}
{/* Workspace API Billing Settings */}
{/* Billed Account for Workspace */}
{canManageWorkspaceKeys && (
<div className='mt-[24px] flex items-center justify-between'>
<span className='font-medium text-[13px]'>Billed Account for Workspace</span>
<div className='flex items-center justify-between'>
<Label>Billed Account for Workspace</Label>
{isWorkspaceLoading ? (
<Skeleton className='h-8 w-[200px] rounded-[6px]' />
) : workspaceAdmins.length === 0 ? (
<div className='rounded-[6px] border border-dashed px-3 py-1.5 text-[var(--text-muted)] text-xs'>
<div className='rounded-[6px] border border-dashed px-3 py-1.5 text-[12px] text-[var(--text-muted)]'>
No admin members available
</div>
) : (
<Select
value={billedAccountUserId ?? ''}
onValueChange={async (value) => {
if (value === billedAccountUserId) return
try {
await updateWorkspaceSettings({ billedAccountUserId: value })
} catch (error) {
// Error is already logged in updateWorkspaceSettings
}
}}
disabled={!canManageWorkspaceKeys || updateWorkspaceMutation.isPending}
>
<SelectTrigger className='h-8 w-[200px] justify-between text-left text-xs'>
<SelectValue placeholder='Select admin' />
</SelectTrigger>
<SelectContent align='start' className='z-[10000050]'>
<SelectGroup>
<SelectLabel className='px-3 py-1 text-[11px] text-[var(--text-muted)] uppercase'>
Workspace admins
</SelectLabel>
{workspaceAdmins.map((admin: any) => (
<SelectItem key={admin.userId} value={admin.userId} className='py-1 text-xs'>
{admin.email}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Popover>
<PopoverTrigger asChild>
<button
className='flex h-8 w-[200px] items-center justify-between gap-2 rounded-[6px] border border-[var(--border)] bg-transparent px-3 text-left text-[13px] transition-colors hover:bg-[var(--surface-3)] disabled:pointer-events-none disabled:opacity-50'
disabled={!canManageWorkspaceKeys || updateWorkspaceMutation.isPending}
>
<span className='flex-1 truncate text-[var(--text-primary)]'>
{billedAccountUserId
? workspaceAdmins.find((admin: any) => admin.userId === billedAccountUserId)
?.email || 'Select admin'
: 'Select admin'}
</span>
<ChevronDown className='h-3 w-3 shrink-0 text-[var(--text-secondary)]' />
</button>
</PopoverTrigger>
<PopoverContent align='end' minWidth={200} border>
<PopoverSection>Workspace admins</PopoverSection>
{workspaceAdmins.map((admin: any) => (
<PopoverItem
key={admin.userId}
active={billedAccountUserId === admin.userId}
showCheck
onClick={async () => {
if (admin.userId === billedAccountUserId) return
try {
await updateWorkspaceSettings({ billedAccountUserId: admin.userId })
} catch (error) {
// Error is already logged in updateWorkspaceSettings
}
}}
>
<span className='flex-1 truncate'>{admin.email}</span>
</PopoverItem>
))}
</PopoverContent>
</Popover>
)}
</div>
)}
@@ -614,11 +619,14 @@ function BillingUsageNotificationsToggle() {
return (
<div className='flex items-center justify-between'>
<div className='flex flex-col'>
<span className='font-medium text-[13px]'>Usage notifications</span>
<span className='text-[var(--text-muted)] text-xs'>Email me when I reach 80% usage</span>
<div className='flex flex-col gap-[2px]'>
<Label htmlFor='usage-notifications'>Usage notifications</Label>
<span className='text-[12px] text-[var(--text-muted)]'>
Email me when I reach 80% usage
</span>
</div>
<Switch
id='usage-notifications'
checked={!!enabled}
disabled={isLoading}
onCheckedChange={(v: boolean) => {

View File

@@ -154,7 +154,7 @@ export function TeamMembers({
<div className='space-y-4'>
{teamItems.map((item) => (
<div key={item.id} className='flex items-center justify-between'>
{/* Member info */}
{/* Left section: Avatar + Name/Role + Action buttons */}
<div className='flex flex-1 items-center gap-3'>
{/* Avatar */}
<UserAvatar
@@ -165,7 +165,7 @@ export function TeamMembers({
/>
{/* Name and email */}
<div className='min-w-0 flex-1'>
<div className='min-w-0'>
<div className='flex items-center gap-2'>
<span className='truncate font-medium text-sm'>{item.name}</span>
{item.type === 'member' && (
@@ -188,51 +188,50 @@ export function TeamMembers({
<div className='truncate text-[var(--text-muted)] text-xs'>{item.email}</div>
</div>
{/* Usage stats - matching subscription layout */}
{/* Action buttons */}
{isAdminOrOwner && (
<div className='hidden items-center text-xs tabular-nums sm:flex'>
<div className='text-center'>
<div className='text-[var(--text-muted)]'>Usage</div>
<div className='font-medium'>
{isLoadingUsage && item.type === 'member' ? (
<span className='inline-block h-3 w-12 animate-pulse rounded bg-[var(--surface-3)]' />
) : (
item.usage
)}
</div>
</div>
<>
{/* Admin/Owner can remove other members */}
{item.type === 'member' &&
item.role !== 'owner' &&
item.email !== currentUserEmail && (
<Button
variant='ghost'
onClick={() => onRemoveMember(item.member)}
className='h-8 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
>
Remove
</Button>
)}
{/* Admin can cancel invitations */}
{item.type === 'invitation' && (
<Button
variant='ghost'
onClick={() => handleCancelInvitation(item.invitation.id)}
disabled={cancellingInvitations.has(item.invitation.id)}
className='h-8 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
>
{cancellingInvitations.has(item.invitation.id) ? 'Cancelling...' : 'Cancel'}
</Button>
)}
</>
)}
</div>
{/* Right section: Usage column (right-aligned) */}
{isAdminOrOwner && (
<div className='ml-4 flex flex-col items-end'>
<div className='text-[var(--text-muted)] text-xs'>Usage</div>
<div className='font-medium text-xs tabular-nums'>
{isLoadingUsage && item.type === 'member' ? (
<span className='inline-block h-3 w-12 animate-pulse rounded bg-[var(--surface-3)]' />
) : (
item.usage
)}
</div>
)}
</div>
{/* Actions */}
<div className='ml-4 flex gap-1'>
{/* Admin/Owner can remove other members */}
{isAdminOrOwner &&
item.type === 'member' &&
item.role !== 'owner' &&
item.email !== currentUserEmail && (
<Button
variant='ghost'
onClick={() => onRemoveMember(item.member)}
className='h-8 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
>
Remove
</Button>
)}
{/* Admin can cancel invitations */}
{isAdminOrOwner && item.type === 'invitation' && (
<Button
variant='ghost'
onClick={() => handleCancelInvitation(item.invitation.id)}
disabled={cancellingInvitations.has(item.invitation.id)}
className='h-8 text-[var(--text-muted)] hover:text-[var(--text-primary)]'
>
{cancellingInvitations.has(item.invitation.id) ? 'Cancelling...' : 'Cancel'}
</Button>
)}
</div>
</div>
)}
</div>
))}
</div>

View File

@@ -5,11 +5,10 @@ import {
type ComboboxOption,
Label,
Modal,
ModalBody,
ModalContent,
ModalDescription,
ModalFooter,
ModalHeader,
ModalTitle,
Tooltip,
} from '@/components/emcn'
import { DEFAULT_TEAM_TIER_COST_LIMIT } from '@/lib/billing/constants'
@@ -55,50 +54,53 @@ export function TeamSeats({
const totalMonthlyCost = selectedSeats * costPerSeat
const costChange = currentSeats ? (selectedSeats - currentSeats) * costPerSeat : 0
const handleConfirm = async () => {
await onConfirm(selectedSeats)
}
const seatOptions: ComboboxOption[] = [1, 2, 3, 4, 5, 10, 15, 20, 25, 30, 40, 50].map((num) => ({
value: num.toString(),
label: `${num} ${num === 1 ? 'seat' : 'seats'} ($${num * costPerSeat}/month)`,
label: `${num} ${num === 1 ? 'seat' : 'seats'}`,
}))
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent>
<ModalHeader>
<ModalTitle>{title}</ModalTitle>
<ModalDescription>{description}</ModalDescription>
</ModalHeader>
<ModalContent size='sm'>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-muted)]'>{description}</p>
<div className='py-4'>
<Label htmlFor='seats'>Number of seats</Label>
<Combobox
options={seatOptions}
value={selectedSeats.toString()}
onChange={(value) => setSelectedSeats(Number.parseInt(value))}
placeholder='Select number of seats'
/>
<div className='mt-4 flex flex-col gap-[4px]'>
<Label htmlFor='seats'>Number of seats</Label>
<Combobox
options={seatOptions}
value={selectedSeats > 0 ? selectedSeats.toString() : ''}
onChange={(value) => {
const num = Number.parseInt(value, 10)
if (!Number.isNaN(num) && num > 0) {
setSelectedSeats(num)
}
}}
placeholder='Select or enter number of seats'
editable
disabled={isLoading}
/>
</div>
<p className='mt-2 text-[var(--text-muted)] text-sm'>
<p className='mt-3 text-[12px] text-[var(--text-muted)]'>
Your team will have {selectedSeats} {selectedSeats === 1 ? 'seat' : 'seats'} with a
total of ${totalMonthlyCost} inference credits per month.
</p>
{showCostBreakdown && currentSeats !== undefined && (
<div className='mt-3 rounded-[8px] bg-[var(--surface-3)] p-3'>
<div className='flex justify-between text-sm'>
<div className='mt-4 rounded-[6px] bg-[var(--surface-5)] p-3'>
<div className='flex justify-between text-[12px]'>
<span className='text-[var(--text-muted)]'>Current seats:</span>
<span>{currentSeats}</span>
<span className='text-[var(--text-primary)]'>{currentSeats}</span>
</div>
<div className='flex justify-between text-sm'>
<div className='mt-2 flex justify-between text-[12px]'>
<span className='text-[var(--text-muted)]'>New seats:</span>
<span>{selectedSeats}</span>
<span className='text-[var(--text-primary)]'>{selectedSeats}</span>
</div>
<div className='mt-2 flex justify-between border-t pt-2 font-medium text-sm'>
<span className='text-[var(--text-muted)]'>Monthly cost change:</span>
<span>
<div className='mt-3 flex justify-between border-[var(--border)] border-t pt-3 text-[12px]'>
<span className='font-medium text-[var(--text-primary)]'>Monthly cost change:</span>
<span className='font-medium text-[var(--text-primary)]'>
{costChange > 0 ? '+' : ''}${costChange}
</span>
</div>
@@ -106,19 +108,14 @@ export function TeamSeats({
)}
{error && (
<p className='mt-3 text-[#DC2626] text-[11px] leading-tight dark:text-[#F87171]'>
<p className='mt-3 text-[12px] text-[var(--text-error)]'>
{error instanceof Error && error.message ? error.message : String(error)}
</p>
)}
</div>
</ModalBody>
<ModalFooter>
<Button
variant='outline'
onClick={() => onOpenChange(false)}
disabled={isLoading}
className='h-[32px] px-[12px]'
>
<Button onClick={() => onOpenChange(false)} disabled={isLoading}>
Cancel
</Button>
@@ -127,22 +124,15 @@ export function TeamSeats({
<span>
<Button
variant='primary'
onClick={handleConfirm}
onClick={() => onConfirm(selectedSeats)}
disabled={
isLoading ||
selectedSeats < 1 ||
(showCostBreakdown && selectedSeats === currentSeats) ||
isCancelledAtPeriodEnd
}
className='h-[32px] px-[12px]'
>
{isLoading ? (
<div className='flex items-center space-x-2'>
<div className='h-4 w-4 animate-spin rounded-full border-2 border-current border-b-transparent' />
<span>Loading...</span>
</div>
) : (
<span>{confirmButtonText}</span>
)}
{isLoading ? 'Updating...' : confirmButtonText}
</Button>
</span>
</Tooltip.Trigger>