CodeBuddy CN bonus packs ("Bonus Pack N") are one-shot credits whose
CycleEndTime equals DeductionEndTime — they expire for good and never
replenish. The dashboard rendered their resetAt as "Reset in Xd",
implying a monthly refill.
Tag bonus packs recurring:false (refill packs recurring:true) in the
usage handler, forward the flag through parseQuotaData, and word the
quota table / progress bar as "Expires in" / "Expires at" for
one-shot packs.
133 lines
3.5 KiB
JavaScript
133 lines
3.5 KiB
JavaScript
"use client";
|
|
|
|
import { cn } from "@/shared/utils/cn";
|
|
import { formatResetTime } from "./utils";
|
|
|
|
// Calculate color based on remaining percentage
|
|
const getColorClasses = (remainingPercentage) => {
|
|
if (remainingPercentage > 70) {
|
|
return {
|
|
text: "text-green-500",
|
|
bg: "bg-green-500",
|
|
bgLight: "bg-green-500/10",
|
|
emoji: "🟢"
|
|
};
|
|
}
|
|
|
|
if (remainingPercentage >= 30) {
|
|
return {
|
|
text: "text-yellow-500",
|
|
bg: "bg-yellow-500",
|
|
bgLight: "bg-yellow-500/10",
|
|
emoji: "🟡"
|
|
};
|
|
}
|
|
|
|
// 0-29% including 0% (out of quota) - show red
|
|
return {
|
|
text: "text-red-500",
|
|
bg: "bg-red-500",
|
|
bgLight: "bg-red-500/10",
|
|
emoji: "🔴"
|
|
};
|
|
};
|
|
|
|
// Format reset time display
|
|
const formatResetTimeDisplay = (resetTime) => {
|
|
if (!resetTime) return null;
|
|
|
|
try {
|
|
const resetDate = new Date(resetTime);
|
|
const now = new Date();
|
|
const isToday = resetDate.toDateString() === now.toDateString();
|
|
const isTomorrow = resetDate.toDateString() === new Date(now.getTime() + 86400000).toDateString();
|
|
|
|
const timeStr = resetDate.toLocaleTimeString(undefined, {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
});
|
|
|
|
if (isToday) return `Today, ${timeStr}`;
|
|
if (isTomorrow) return `Tomorrow, ${timeStr}`;
|
|
|
|
return resetDate.toLocaleString(undefined, {
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
});
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export default function QuotaProgressBar({
|
|
percentage = 0,
|
|
label = "",
|
|
used = 0,
|
|
total = 0,
|
|
unlimited = false,
|
|
resetTime = null,
|
|
recurring = true,
|
|
}) {
|
|
const colors = getColorClasses(percentage);
|
|
const countdown = formatResetTime(resetTime);
|
|
const resetDisplay = formatResetTimeDisplay(resetTime);
|
|
|
|
// recurring defaults true. One-shot packs (e.g. CodeBuddy CN bonus packs)
|
|
// set recurring:false: resetTime is a hard expiry, so word it as "expires".
|
|
const resetWord = recurring ? "Reset" : "Expires";
|
|
|
|
// percentage is already remaining percentage (from ProviderLimitCard)
|
|
const remaining = percentage;
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{/* Label and percentage */}
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="font-semibold text-text-primary">
|
|
{label}
|
|
</span>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-xs">{colors.emoji}</span>
|
|
<span className={cn("font-medium", colors.text)}>
|
|
{remaining}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
{!unlimited && (
|
|
<div className={cn("h-2 rounded-full overflow-hidden", colors.bgLight)}>
|
|
<div
|
|
className={cn("h-full transition-all duration-300", colors.bg)}
|
|
style={{ width: `${Math.min(remaining, 100)}%` }}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Usage details and countdown */}
|
|
<div className="flex items-center justify-between text-xs text-text-muted">
|
|
<span>
|
|
{used.toLocaleString()} / {total.toLocaleString()} requests
|
|
</span>
|
|
{countdown !== "-" && (
|
|
<div className="flex items-center gap-1">
|
|
<span>•</span>
|
|
<span className="font-medium">{resetWord} in {countdown}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Reset time display */}
|
|
{resetDisplay && (
|
|
<div className="text-xs text-text-muted/70">
|
|
{resetWord} at {resetDisplay}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|