132 lines
3.7 KiB
TypeScript
132 lines
3.7 KiB
TypeScript
|
|
/**
|
||
|
|
* Rate Limit Indicator Component
|
||
|
|
* Action 5.4.1.2: Display rate limit status to users
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { useEffect, useState } from 'react';
|
||
|
|
import { useRateLimitStore } from '@/stores/rateLimit';
|
||
|
|
import { AlertTriangle, Clock } from 'lucide-react';
|
||
|
|
import { cn } from '@/lib/utils';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* RateLimitIndicator - Displays current rate limit status
|
||
|
|
*
|
||
|
|
* Shows rate limit information when:
|
||
|
|
* - User is rate limited (isLimited = true)
|
||
|
|
* - Remaining requests are low (< 20% of limit)
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```tsx
|
||
|
|
* <RateLimitIndicator />
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function RateLimitIndicator() {
|
||
|
|
const { limit, remaining, reset, isLimited } = useRateLimitStore();
|
||
|
|
const [timeUntilReset, setTimeUntilReset] = useState<number | null>(null);
|
||
|
|
|
||
|
|
// Calculate time until reset
|
||
|
|
useEffect(() => {
|
||
|
|
if (!reset) {
|
||
|
|
setTimeUntilReset(null);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const updateTimer = () => {
|
||
|
|
const now = Math.floor(Date.now() / 1000);
|
||
|
|
const secondsUntilReset = reset - now;
|
||
|
|
setTimeUntilReset(secondsUntilReset > 0 ? secondsUntilReset : 0);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Update immediately
|
||
|
|
updateTimer();
|
||
|
|
|
||
|
|
// Update every second
|
||
|
|
const interval = setInterval(updateTimer, 1000);
|
||
|
|
|
||
|
|
return () => clearInterval(interval);
|
||
|
|
}, [reset]);
|
||
|
|
|
||
|
|
// Calculate percentage remaining
|
||
|
|
const percentageRemaining =
|
||
|
|
limit !== null && remaining !== null && limit > 0
|
||
|
|
? (remaining / limit) * 100
|
||
|
|
: null;
|
||
|
|
|
||
|
|
// Show indicator if:
|
||
|
|
// 1. User is rate limited, OR
|
||
|
|
// 2. Remaining is low (< 20% of limit) and we have data
|
||
|
|
const shouldShow =
|
||
|
|
isLimited ||
|
||
|
|
(limit !== null &&
|
||
|
|
remaining !== null &&
|
||
|
|
percentageRemaining !== null &&
|
||
|
|
percentageRemaining < 20);
|
||
|
|
|
||
|
|
if (!shouldShow || limit === null) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Format time until reset
|
||
|
|
const formatTimeUntilReset = (seconds: number): string => {
|
||
|
|
if (seconds <= 0) return '0s';
|
||
|
|
if (seconds < 60) return `${seconds}s`;
|
||
|
|
const minutes = Math.floor(seconds / 60);
|
||
|
|
const remainingSeconds = seconds % 60;
|
||
|
|
if (minutes < 60) {
|
||
|
|
return remainingSeconds > 0
|
||
|
|
? `${minutes}m ${remainingSeconds}s`
|
||
|
|
: `${minutes}m`;
|
||
|
|
}
|
||
|
|
const hours = Math.floor(minutes / 60);
|
||
|
|
const remainingMinutes = minutes % 60;
|
||
|
|
return remainingMinutes > 0
|
||
|
|
? `${hours}h ${remainingMinutes}m`
|
||
|
|
: `${hours}h`;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Determine severity and styling
|
||
|
|
const isCritical = isLimited || (remaining !== null && remaining <= 0);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-all',
|
||
|
|
isCritical
|
||
|
|
? 'bg-kodo-red/10 text-kodo-red border border-kodo-red/30'
|
||
|
|
: 'bg-kodo-gold/10 text-kodo-gold border border-kodo-gold/30',
|
||
|
|
)}
|
||
|
|
role="alert"
|
||
|
|
aria-live="polite"
|
||
|
|
>
|
||
|
|
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
{isLimited ? (
|
||
|
|
<>
|
||
|
|
<span>Rate limit exceeded</span>
|
||
|
|
{timeUntilReset !== null && (
|
||
|
|
<span className="flex items-center gap-1">
|
||
|
|
<Clock className="w-3 h-3" />
|
||
|
|
{formatTimeUntilReset(timeUntilReset)}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<span>
|
||
|
|
{remaining !== null
|
||
|
|
? `${remaining}/${limit} requests`
|
||
|
|
: `${limit} requests`}
|
||
|
|
</span>
|
||
|
|
{timeUntilReset !== null && (
|
||
|
|
<span className="flex items-center gap-1 opacity-75">
|
||
|
|
<Clock className="w-3 h-3" />
|
||
|
|
resets in {formatTimeUntilReset(timeUntilReset)}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|