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

View File

@@ -4,7 +4,9 @@ import { useState } from 'react'
import { import {
Button, Button,
Input, Input,
Label,
Modal, Modal,
ModalBody,
ModalClose, ModalClose,
ModalContent, ModalContent,
ModalFooter, ModalFooter,
@@ -90,7 +92,6 @@ export function CreditBalance({
const handleOpenChange = (open: boolean) => { const handleOpenChange = (open: boolean) => {
setIsOpen(open) setIsOpen(open)
if (open) { if (open) {
// Generate new requestId when modal opens - same ID used for entire session
setRequestId(crypto.randomUUID()) setRequestId(crypto.randomUUID())
} else { } else {
setAmount('') setAmount('')
@@ -102,72 +103,66 @@ export function CreditBalance({
return ( return (
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-[8px]'>
<span className='text-muted-foreground text-sm'>Credit Balance</span> <Label>Credit Balance</Label>
<span className='font-medium text-sm'>{isLoading ? '...' : `$${balance.toFixed(2)}`}</span> <span className='text-[13px] text-[var(--text-secondary)]'>
{isLoading ? '...' : `$${balance.toFixed(2)}`}
</span>
</div> </div>
{canPurchase && ( {canPurchase && (
<Modal open={isOpen} onOpenChange={handleOpenChange}> <Modal open={isOpen} onOpenChange={handleOpenChange}>
<ModalTrigger asChild> <ModalTrigger asChild>
<Button variant='outline'>Add Credits</Button> <Button variant='outline' className='h-8 rounded-[8px] text-[13px]'>
Add Credits
</Button>
</ModalTrigger> </ModalTrigger>
<ModalContent> <ModalContent size='sm'>
<ModalHeader>Add Credits</ModalHeader> <ModalHeader>Add Credits</ModalHeader>
<div className='px-4'> <ModalBody>
<p className='text-[13px] text-[var(--text-secondary)]'> {success ? (
Credits are used before overage charges. Min $10, max $1,000. <p className='text-center text-[13px] text-[var(--text-primary)]'>
</p>
</div>
{success ? (
<div className='py-4 text-center'>
<p className='text-[14px] text-[var(--text-primary)]'>
Credits added successfully! Credits added successfully!
</p> </p>
</div> ) : (
) : ( <>
<div className='flex flex-col gap-3 py-2'> <p className='text-[12px] text-[var(--text-muted)]'>
<div className='flex flex-col gap-1'> Credits are used before overage charges. Min $10, max $1,000.
<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> </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 && ( {!success && (
<ModalFooter> <ModalFooter>
<ModalClose asChild> <ModalClose asChild>
<Button variant='ghost' disabled={isPurchasing}> <Button disabled={isPurchasing}>Cancel</Button>
Cancel
</Button>
</ModalClose> </ModalClose>
<Button <Button
variant='primary' variant='primary'

View File

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

View File

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

View File

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

View File

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