Fixed empty "Resets on" date display for new users: Issue: - Users who haven't created any conversations yet have limitResetDate = NULL - The "Resets on" field was showing empty/blank - This confused users about when their limit would reset Solution: - Updated formatResetDate() in 3 components to calculate default date - If limitResetDate is NULL, display "1 month from now" - This gives users a clear expectation of when limits reset Files Updated: - app/[locale]/subscription/page.tsx * formatResetDate() now returns calculated date if null - components/subscription/usage-display.tsx * formatResetDate() now returns calculated date if null - components/subscription/upgrade-modal.tsx * formatResetDate() now returns calculated date if null * Removed conditional check - always show reset date User Experience: - New users see "Resets on: [date one month from now]" - Once they create their first conversation, actual reset date is set - Consistent messaging across all subscription UI components Note: The actual limitResetDate is set when the user creates their first conversation (in incrementConversationCount function). This fix only affects the UI display for users who haven't chatted yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
Box,
|
|
Typography,
|
|
LinearProgress,
|
|
Chip,
|
|
Button,
|
|
Paper,
|
|
CircularProgress,
|
|
Skeleton
|
|
} from '@mui/material'
|
|
import {
|
|
TrendingUp,
|
|
AutoAwesome
|
|
} from '@mui/icons-material'
|
|
import { useTranslations, useLocale } from 'next-intl'
|
|
import Link from 'next/link'
|
|
|
|
interface UsageData {
|
|
tier: string
|
|
status: string
|
|
conversationLimit: number
|
|
conversationCount: number
|
|
limitResetDate: string | null
|
|
}
|
|
|
|
interface UsageDisplayProps {
|
|
compact?: boolean
|
|
showUpgradeButton?: boolean
|
|
}
|
|
|
|
export default function UsageDisplay({ compact = false, showUpgradeButton = true }: UsageDisplayProps) {
|
|
const locale = useLocale()
|
|
const t = useTranslations('subscription.usage')
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
const [usageData, setUsageData] = useState<UsageData | null>(null)
|
|
const [error, setError] = useState(false)
|
|
|
|
useEffect(() => {
|
|
fetchUsageData()
|
|
}, [])
|
|
|
|
const fetchUsageData = async () => {
|
|
try {
|
|
const token = localStorage.getItem('authToken')
|
|
if (!token) {
|
|
setError(true)
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
const response = await fetch('/api/user/profile', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setUsageData({
|
|
tier: data.user.subscriptionTier || 'free',
|
|
status: data.user.subscriptionStatus || 'active',
|
|
conversationLimit: data.user.conversationLimit || 10,
|
|
conversationCount: data.user.conversationCount || 0,
|
|
limitResetDate: data.user.limitResetDate
|
|
})
|
|
} else {
|
|
setError(true)
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching usage data:', err)
|
|
setError(true)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const formatResetDate = (dateString: string | null) => {
|
|
if (!dateString) {
|
|
// If no reset date set, calculate 1 month from now
|
|
const nextMonth = new Date()
|
|
nextMonth.setMonth(nextMonth.getMonth() + 1)
|
|
return nextMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' })
|
|
}
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' })
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<Paper elevation={1} sx={{ p: compact ? 2 : 3 }}>
|
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 2 }}>
|
|
<Skeleton variant="rectangular" width={80} height={24} />
|
|
<Skeleton variant="circular" width={24} height={24} />
|
|
</Box>
|
|
<Skeleton variant="rectangular" height={8} sx={{ mb: 1 }} />
|
|
<Skeleton variant="text" width="60%" />
|
|
</Paper>
|
|
)
|
|
}
|
|
|
|
if (error || !usageData) {
|
|
return null
|
|
}
|
|
|
|
const isPremium = usageData.tier === 'premium'
|
|
const usagePercentage = isPremium ? 0 : (usageData.conversationCount / usageData.conversationLimit) * 100
|
|
const remaining = Math.max(0, usageData.conversationLimit - usageData.conversationCount)
|
|
const isNearLimit = !isPremium && remaining <= 2
|
|
|
|
return (
|
|
<Paper elevation={1} sx={{ p: compact ? 2 : 3 }}>
|
|
{/* Tier Badge */}
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography variant={compact ? 'body2' : 'h6'} fontWeight="600">
|
|
{t('title')}
|
|
</Typography>
|
|
<Chip
|
|
label={isPremium ? t('premiumTier') : t('freeTier')}
|
|
color={isPremium ? 'primary' : 'default'}
|
|
size="small"
|
|
icon={isPremium ? <AutoAwesome /> : undefined}
|
|
sx={{ fontWeight: 600 }}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Usage Stats */}
|
|
<Box>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
|
<Typography variant={compact ? 'caption' : 'body2'} color="text.secondary">
|
|
{t('conversations')}
|
|
</Typography>
|
|
<Typography variant={compact ? 'caption' : 'body2'} fontWeight="600">
|
|
{isPremium ? (
|
|
t('unlimited')
|
|
) : (
|
|
`${usageData.conversationCount} ${t('of')} ${usageData.conversationLimit}`
|
|
)}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{!isPremium && (
|
|
<>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={Math.min(usagePercentage, 100)}
|
|
sx={{
|
|
height: compact ? 6 : 8,
|
|
borderRadius: 1,
|
|
bgcolor: 'grey.200',
|
|
'& .MuiLinearProgress-bar': {
|
|
bgcolor: isNearLimit ? 'warning.main' : 'primary.main'
|
|
}
|
|
}}
|
|
/>
|
|
<Typography
|
|
variant="caption"
|
|
color={isNearLimit ? 'warning.main' : 'text.secondary'}
|
|
sx={{ mt: 1, display: 'block' }}
|
|
>
|
|
{remaining} {t('remaining')}
|
|
{usageData.limitResetDate && (
|
|
<> • {t('resetsOn')} {formatResetDate(usageData.limitResetDate)}</>
|
|
)}
|
|
</Typography>
|
|
</>
|
|
)}
|
|
|
|
{isPremium && (
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
|
{t('premiumDescription')}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Upgrade Button */}
|
|
{!isPremium && showUpgradeButton && (
|
|
<Button
|
|
variant={isNearLimit ? 'contained' : 'outlined'}
|
|
size={compact ? 'small' : 'medium'}
|
|
fullWidth
|
|
startIcon={<TrendingUp />}
|
|
component={Link}
|
|
href={`/${locale}/subscription`}
|
|
sx={{ mt: 2 }}
|
|
>
|
|
{t('upgradeButton')}
|
|
</Button>
|
|
)}
|
|
</Paper>
|
|
)
|
|
}
|