chore: update credit expiration messaging and logic to reflect upcoming expiration in days

This commit is contained in:
javayhu 2025-08-23 09:52:28 +08:00
parent 6837c5a8d4
commit 01f5734dd5
5 changed files with 45 additions and 42 deletions

View File

@ -601,8 +601,7 @@
"creditsAdded": "Credits have been added to your account",
"viewTransactions": "View Credit Transactions",
"retry": "Retry",
"expiringCredits": "{credits} credits expiring on {date}"
"expiringCredits": "{credits} credits expiring in the next {days} days"
},
"packages": {
"title": "Credit Packages",

View File

@ -601,8 +601,7 @@
"creditsAdded": "积分已添加到您的账户",
"viewTransactions": "查看积分记录",
"retry": "重试",
"expiringCredits": "{credits} 积分将在 {date} 过期"
"expiringCredits": "{credits} 积分将在 {days} 天内过期"
},
"packages": {
"title": "积分套餐",

View File

@ -3,11 +3,10 @@
import { getDb } from '@/db';
import { creditTransaction } from '@/db/schema';
import type { User } from '@/lib/auth-types';
import { CREDITS_EXPIRATION_DAYS } from '@/lib/constants';
import { userActionClient } from '@/lib/safe-action';
import { addDays } from 'date-fns';
import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm';
const CREDITS_EXPIRATION_DAYS = 31;
import { and, eq, gt, gte, isNotNull, lte, sum } from 'drizzle-orm';
/**
* Get credit statistics for a user
@ -18,12 +17,14 @@ export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => {
const userId = currentUser.id;
const db = await getDb();
// Get credits expiring in the next CREDITS_EXPIRATION_DAYS days
const expirationDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS);
const expiringCredits = await db
const now = new Date();
// Get credits expiring in the next 30 days
const expirationDaysFromNow = addDays(now, CREDITS_EXPIRATION_DAYS);
// Get total credits expiring in the next 30 days
const expiringCreditsResult = await db
.select({
amount: sum(creditTransaction.remainingAmount),
earliestExpiration: sql<Date>`MIN(${creditTransaction.expirationDate})`,
totalAmount: sum(creditTransaction.remainingAmount),
})
.from(creditTransaction)
.where(
@ -31,18 +32,20 @@ export const getCreditStatsAction = userActionClient.action(async ({ ctx }) => {
eq(creditTransaction.userId, userId),
isNotNull(creditTransaction.expirationDate),
isNotNull(creditTransaction.remainingAmount),
gte(creditTransaction.remainingAmount, 1),
gt(creditTransaction.remainingAmount, 0),
lte(creditTransaction.expirationDate, expirationDaysFromNow),
gte(creditTransaction.expirationDate, new Date())
gte(creditTransaction.expirationDate, now)
)
);
const totalExpiringCredits =
Number(expiringCreditsResult[0]?.totalAmount) || 0;
return {
success: true,
data: {
expiringCredits: {
amount: Number(expiringCredits[0]?.amount) || 0,
earliestExpiration: expiringCredits[0]?.earliestExpiration || null,
amount: totalExpiringCredits,
},
},
};

View File

@ -14,7 +14,7 @@ import { websiteConfig } from '@/config/website';
import { useCreditBalance, useCreditStats } from '@/hooks/use-credits';
import { useMounted } from '@/hooks/use-mounted';
import { useLocaleRouter } from '@/i18n/navigation';
import { formatDate } from '@/lib/formatter';
import { CREDITS_EXPIRATION_DAYS } from '@/lib/constants';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { RefreshCwIcon } from 'lucide-react';
@ -102,13 +102,12 @@ export default function CreditsBalanceCard() {
</CardHeader>
<CardContent className="space-y-4 flex-1">
<div className="flex items-center justify-start space-x-4">
<Skeleton className="h-6 w-1/5" />
</div>
<div className="text-sm text-muted-foreground space-y-2">
<Skeleton className="h-6 w-3/5" />
<Skeleton className="h-8 w-1/5" />
</div>
</CardContent>
<CardFooter className="">{/* show nothing */}</CardFooter>
<CardFooter className="px-6 py-4 flex justify-between items-center bg-background rounded-none">
<Skeleton className="h-6 w-3/5" />
</CardFooter>
</Card>
);
}
@ -147,7 +146,7 @@ export default function CreditsBalanceCard() {
<CardDescription>{t('description')}</CardDescription>
</CardHeader>
<CardContent className="flex-1">
{/* Credits balance display */}
{/* Credits balance */}
<div className="flex items-center justify-start space-x-4">
<div className="flex items-center space-x-2">
{/* <CoinsIcon className="h-6 w-6 text-muted-foreground" /> */}
@ -159,25 +158,20 @@ export default function CreditsBalanceCard() {
</div>
</CardContent>
<CardFooter className="px-6 py-4 flex justify-between items-center bg-background rounded-none">
{/* Balance information */}
<div className="text-sm text-muted-foreground space-y-2">
{/* Expiring credits warning */}
{!isLoadingStats &&
creditStats &&
creditStats.expiringCredits.amount > 0 &&
creditStats.expiringCredits.earliestExpiration && (
<div className="flex items-center gap-2 text-amber-600">
<span>
{t('expiringCredits', {
credits: creditStats.expiringCredits.amount,
date: formatDate(
new Date(creditStats.expiringCredits.earliestExpiration)
),
})}
</span>
</div>
)}
</div>
{/* Expiring credits warning */}
{!isLoadingStats && creditStats && (
<div className="text-sm text-muted-foreground space-y-2">
{' '}
<div className="flex items-center gap-2 text-amber-600">
<span>
{t('expiringCredits', {
credits: creditStats.expiringCredits.amount,
days: CREDITS_EXPIRATION_DAYS,
})}
</span>
</div>
</div>
)}
</CardFooter>
</Card>
);

View File

@ -1,2 +1,10 @@
/**
* in next 30 days for credits expiration
*/
export const CREDITS_EXPIRATION_DAYS = 30;
/**
* placeholder image for blog post card
*/
export const PLACEHOLDER_IMAGE =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg==';