mirror of
https://github.com/YouXam/claude-code-usage-dashboard.git
synced 2025-12-21 05:29:10 +08:00
improve loading animation
This commit is contained in:
@@ -43,14 +43,19 @@ interface CurrentPeriodProps {
|
|||||||
export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {
|
export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {
|
||||||
const [summary, setSummary] = useState<PeriodSummary | null>(null);
|
const [summary, setSummary] = useState<PeriodSummary | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSummary();
|
fetchSummary();
|
||||||
}, [period.index]);
|
}, [period.index]);
|
||||||
|
|
||||||
const fetchSummary = async () => {
|
const fetchSummary = async (isRefresh = false) => {
|
||||||
|
if (isRefresh) {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
} else {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
}
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -69,8 +74,12 @@ export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load period data');
|
setError(err instanceof Error ? err.message : 'Failed to load period data');
|
||||||
} finally {
|
} finally {
|
||||||
|
if (isRefresh) {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
} else {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string | null, isEndDate = false) => {
|
const formatDate = (dateString: string | null, isEndDate = false) => {
|
||||||
@@ -103,13 +112,71 @@ export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* Header skeleton */}
|
||||||
<div className="bg-card p-6 rounded-lg shadow-sm border border-border animate-pulse">
|
<div className="bg-card p-6 rounded-lg shadow-sm border border-border animate-pulse">
|
||||||
<div className="h-6 bg-muted rounded w-1/3 mb-4"></div>
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="h-6 bg-muted rounded w-2/3"></div>
|
||||||
<div className="h-20 bg-muted rounded"></div>
|
<div className="h-8 w-20 bg-muted rounded"></div>
|
||||||
<div className="h-20 bg-muted rounded"></div>
|
|
||||||
<div className="h-20 bg-muted rounded"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="h-8 bg-muted rounded w-24 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-16 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="h-8 bg-muted rounded w-16 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-20 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="h-8 bg-muted rounded w-20 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-32 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ranking table skeleton */}
|
||||||
|
<div className="bg-card rounded-lg shadow-sm border border-border animate-pulse">
|
||||||
|
<div className="p-6 border-b border-border">
|
||||||
|
<div className="h-6 bg-muted rounded w-32"></div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-12"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-16"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-16"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-20"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-16"></div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="px-6 py-4"><div className="h-6 w-6 bg-muted rounded-full"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-20"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-16"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-12"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-12"></div></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User detail card skeleton */}
|
||||||
|
<div className="bg-card p-6 rounded-lg shadow-sm border border-border animate-pulse">
|
||||||
|
<div className="h-6 bg-muted rounded w-48 mb-6"></div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="text-center">
|
||||||
|
<div className="h-6 bg-muted rounded w-16 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-12 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="h-10 bg-muted rounded w-32"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -139,9 +206,26 @@ export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header with period info */}
|
{/* Header with period info */}
|
||||||
<div className="bg-card p-6 rounded-lg shadow-sm border border-border">
|
<div className="bg-card p-6 rounded-lg shadow-sm border border-border">
|
||||||
<h2 className="text-lg font-semibold text-card-foreground mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
Current Period: {formatDate(summary.period.startAt)} → {formatDate(summary.period.endAt, true)}
|
<h2 className="text-lg font-semibold text-card-foreground">
|
||||||
|
Current Period: {formatDate(summary.period.startAt)} → Now
|
||||||
</h2>
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchSummary(true)}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@@ -283,14 +283,71 @@ export function HistoricalPeriods({ periods, apiKey, userId }: HistoricalPeriods
|
|||||||
<>
|
<>
|
||||||
{/* Period Summary */}
|
{/* Period Summary */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
{/* Period summary skeleton */}
|
||||||
<div className="bg-card p-6 rounded-lg shadow-sm border border-border animate-pulse">
|
<div className="bg-card p-6 rounded-lg shadow-sm border border-border animate-pulse">
|
||||||
<div className="h-6 bg-muted rounded w-1/3 mb-4"></div>
|
<div className="h-6 bg-muted rounded w-1/2 mb-4"></div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="h-20 bg-muted rounded"></div>
|
<div className="text-center">
|
||||||
<div className="h-20 bg-muted rounded"></div>
|
<div className="h-8 bg-muted rounded w-24 mx-auto mb-2"></div>
|
||||||
<div className="h-20 bg-muted rounded"></div>
|
<div className="h-4 bg-muted rounded w-16 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="h-8 bg-muted rounded w-16 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-20 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="h-8 bg-muted rounded w-20 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-32 mx-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ranking table skeleton */}
|
||||||
|
<div className="bg-card rounded-lg shadow-sm border border-border animate-pulse">
|
||||||
|
<div className="p-6 border-b border-border">
|
||||||
|
<div className="h-6 bg-muted rounded w-32"></div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-12"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-16"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-16"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-20"></div></th>
|
||||||
|
<th className="px-6 py-3"><div className="h-4 bg-muted rounded w-16"></div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td className="px-6 py-4"><div className="h-6 w-6 bg-muted rounded-full"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-20"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-16"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-12"></div></td>
|
||||||
|
<td className="px-6 py-4"><div className="h-4 bg-muted rounded w-12"></div></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User detail card skeleton */}
|
||||||
|
<div className="bg-card p-6 rounded-lg shadow-sm border border-border animate-pulse">
|
||||||
|
<div className="h-6 bg-muted rounded w-48 mb-6"></div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="text-center">
|
||||||
|
<div className="h-6 bg-muted rounded w-16 mx-auto mb-2"></div>
|
||||||
|
<div className="h-4 bg-muted rounded w-12 mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="h-10 bg-muted rounded w-32"></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="bg-card p-8 rounded-lg shadow-sm border border-border text-center">
|
<div className="bg-card p-8 rounded-lg shadow-sm border border-border text-center">
|
||||||
<div className="text-destructive mb-4">
|
<div className="text-destructive mb-4">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
interface UserDetail {
|
interface UserDetail {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -41,7 +41,7 @@ export function UserDetailCard({ periodIndex, apiKey, userId }: UserDetailCardPr
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
throw new Error('You are not found in this period (possibly deleted)');
|
throw new Error('You are not found in this period');
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to fetch user details: ${response.status}`);
|
throw new Error(`Failed to fetch user details: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -92,13 +92,13 @@ export function UserDetailCard({ periodIndex, apiKey, userId }: UserDetailCardPr
|
|||||||
<h3 className="text-lg font-medium text-card-foreground mb-4">Your Usage Details</h3>
|
<h3 className="text-lg font-medium text-card-foreground mb-4">Your Usage Details</h3>
|
||||||
<div className="bg-chart-3/20 border border-chart-3/50 rounded-md p-4">
|
<div className="bg-chart-3/20 border border-chart-3/50 rounded-md p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0 text-muted-foreground">
|
||||||
<svg className="h-5 w-5 text-chart-3" viewBox="0 0 20 20" fill="currentColor">
|
<svg className="h-5 w-5 text-chart-3" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm text-chart-3">{error}</p>
|
<p className="text-sm text-muted-foreground">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,9 +186,25 @@ export function UserDetailCard({ periodIndex, apiKey, userId }: UserDetailCardPr
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-card-foreground mb-2">Period Start Data</h4>
|
<h4 className="text-sm font-medium text-card-foreground mb-2">Period Start Data</h4>
|
||||||
|
{userDetail.raw.start ? (
|
||||||
<pre className="text-xs bg-muted/50 p-4 rounded-lg overflow-x-auto border border-border">
|
<pre className="text-xs bg-muted/50 p-4 rounded-lg overflow-x-auto border border-border">
|
||||||
{JSON.stringify(userDetail.raw.start, null, 2)}
|
{JSON.stringify(userDetail.raw.start, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs bg-muted/20 p-4 rounded-lg border border-border text-center">
|
||||||
|
<div className="text-muted-foreground mb-2">
|
||||||
|
<svg className="h-8 w-8 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium text-card-foreground mb-1">
|
||||||
|
No Start Data Available
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
This likely means you had no usage before the start of this period.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-card-foreground mb-2">Period End Data</h4>
|
<h4 className="text-sm font-medium text-card-foreground mb-2">Period End Data</h4>
|
||||||
|
|||||||
@@ -242,6 +242,11 @@ export class BillingCalculator {
|
|||||||
|
|
||||||
const result = computePeriodDelta(startData, endData, meId);
|
const result = computePeriodDelta(startData, endData, meId);
|
||||||
|
|
||||||
|
// Filter out users with zero activity (cost, requests, and tokens all 0)
|
||||||
|
const activeUsers = result.users.filter(u =>
|
||||||
|
u.cost > 0 || u.periodTokens > 0 || u.periodRequests > 0
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
period: {
|
period: {
|
||||||
...period,
|
...period,
|
||||||
@@ -249,9 +254,9 @@ export class BillingCalculator {
|
|||||||
},
|
},
|
||||||
totals: {
|
totals: {
|
||||||
totalCost: result.totalCost,
|
totalCost: result.totalCost,
|
||||||
userCount: result.users.filter(u => u.cost > 0).length
|
userCount: activeUsers.length
|
||||||
},
|
},
|
||||||
ranking: result.users
|
ranking: activeUsers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user