Restyle Account usage card like Gemini usage limits
Unit Tests / test (pull_request) Successful in 11s
Unit Tests / test (pull_request) Successful in 11s
Show plan name with prompt/token progress meters and reset hints instead of plain key-value rows.
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { axiosInstance } from '../../../axiosApi';
|
import { axiosInstance } from '../../../axiosApi';
|
||||||
import {
|
import {
|
||||||
|
formatBillingDate,
|
||||||
formatTokenCount,
|
formatTokenCount,
|
||||||
SubscriptionMe,
|
SubscriptionMe,
|
||||||
} from '../../utils/finance';
|
} from '../../utils/finance';
|
||||||
@@ -20,35 +21,72 @@ const GlassCard = styled.div`
|
|||||||
|
|
||||||
const CardTitle = styled.h2`
|
const CardTitle = styled.h2`
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
margin-bottom: 1.5rem;
|
margin: 0 0 0.35rem 0;
|
||||||
color: ${({ theme }) => theme.colors.text};
|
color: ${({ theme }) => theme.colors.text};
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SettingRow = styled.div`
|
const PlanName = styled.p`
|
||||||
display: flex;
|
margin: 0 0 1.75rem 0;
|
||||||
justify-content: space-between;
|
font-size: 1rem;
|
||||||
align-items: center;
|
font-weight: 600;
|
||||||
padding: 1rem 0;
|
color: ${({ theme }) => theme.colors.text};
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
opacity: 0.85;
|
||||||
|
`;
|
||||||
|
|
||||||
&:last-child {
|
const MeterBlock = styled.div`
|
||||||
border-bottom: none;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SettingLabel = styled.span`
|
const MeterHeader = styled.div`
|
||||||
font-size: 1.1rem;
|
display: flex;
|
||||||
color: ${({ theme }) => theme.colors.text};
|
justify-content: space-between;
|
||||||
font-weight: 500;
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SettingValue = styled.span`
|
const MeterLabel = styled.span`
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
color: ${({ theme }) => theme.colors.text};
|
color: ${({ theme }) => theme.colors.text};
|
||||||
opacity: 0.9;
|
`;
|
||||||
text-align: right;
|
|
||||||
|
const MeterMeta = styled.span`
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
opacity: 0.65;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Track = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: ${({ theme }) =>
|
||||||
|
theme.darkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.1)'};
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Fill = styled.div<{ $pct: number; $warn: boolean }>`
|
||||||
|
height: 100%;
|
||||||
|
width: ${({ $pct }) => `${Math.min(100, Math.max(0, $pct))}%`};
|
||||||
|
border-radius: 999px;
|
||||||
|
background: ${({ theme, $warn }) => ($warn ? '#e67e22' : theme.main)};
|
||||||
|
transition: width 0.35s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MeterFooter = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
opacity: 0.65;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BodyText = styled.p`
|
const BodyText = styled.p`
|
||||||
@@ -58,6 +96,49 @@ const BodyText = styled.p`
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const UpgradeHint = styled.p`
|
||||||
|
margin: 1.5rem 0 0 0;
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
border-top: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
opacity: 0.7;
|
||||||
|
line-height: 1.45;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function clampPct(used: number, quota: number): number {
|
||||||
|
if (quota <= 0) return 0;
|
||||||
|
return Math.min(100, Math.round((used / quota) * 1000) / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeterProps = {
|
||||||
|
label: string;
|
||||||
|
meta: string;
|
||||||
|
usedLabel: string;
|
||||||
|
pct: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UsageMeter = ({ label, meta, usedLabel, pct }: MeterProps): JSX.Element => {
|
||||||
|
const showBar = pct !== null;
|
||||||
|
const warn = showBar && pct >= 90;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MeterBlock>
|
||||||
|
<MeterHeader>
|
||||||
|
<MeterLabel>{label}</MeterLabel>
|
||||||
|
<MeterMeta>{meta}</MeterMeta>
|
||||||
|
</MeterHeader>
|
||||||
|
<Track aria-hidden={!showBar}>
|
||||||
|
{showBar ? <Fill $pct={pct} $warn={warn} /> : null}
|
||||||
|
</Track>
|
||||||
|
<MeterFooter>
|
||||||
|
<span>{usedLabel}</span>
|
||||||
|
<span>{showBar ? `${pct}%` : '—'}</span>
|
||||||
|
</MeterFooter>
|
||||||
|
</MeterBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const UsageSummaryCard = (): JSX.Element => {
|
const UsageSummaryCard = (): JSX.Element => {
|
||||||
const [subscription, setSubscription] = useState<SubscriptionMe | null>(null);
|
const [subscription, setSubscription] = useState<SubscriptionMe | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -79,35 +160,73 @@ const UsageSummaryCard = (): JSX.Element => {
|
|||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
const usage = subscription?.usage;
|
const usage = subscription?.usage;
|
||||||
const promptsLabel =
|
const promptQuota = usage?.prompt_quota ?? null;
|
||||||
!usage || usage.prompts_remaining === null || usage.prompt_quota === null
|
const promptsUsed = usage?.prompts_in_window ?? 0;
|
||||||
? '—'
|
const promptPct =
|
||||||
: `${usage.prompts_remaining}/${usage.prompt_quota}`;
|
promptQuota !== null ? clampPct(promptsUsed, promptQuota) : null;
|
||||||
|
|
||||||
|
const tokenQuota = usage?.monthly_token_quota ?? null;
|
||||||
|
const tokensUsed = usage?.tokens_total_period ?? null;
|
||||||
|
const tokenPct =
|
||||||
|
tokenQuota !== null && tokensUsed !== null
|
||||||
|
? clampPct(tokensUsed, tokenQuota)
|
||||||
|
: tokenQuota !== null
|
||||||
|
? 0
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard data-testid="usage-summary-card">
|
<GlassCard data-testid="usage-summary-card">
|
||||||
<CardTitle>Plan & usage</CardTitle>
|
<CardTitle>Usage limit</CardTitle>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<BodyText>Loading usage…</BodyText>
|
<BodyText>Loading usage…</BodyText>
|
||||||
) : !subscription?.plan ? (
|
) : !subscription?.plan ? (
|
||||||
<BodyText>No active plan yet.</BodyText>
|
<BodyText>No active plan yet.</BodyText>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SettingRow>
|
<PlanName>{subscription.plan.name}</PlanName>
|
||||||
<SettingLabel>Plan</SettingLabel>
|
|
||||||
<SettingValue>{subscription.plan.name}</SettingValue>
|
<UsageMeter
|
||||||
</SettingRow>
|
label="Prompts"
|
||||||
<SettingRow>
|
meta={`Resets every ${usage?.window_hours ?? '—'}h`}
|
||||||
<SettingLabel>Prompts left ({usage?.window_hours ?? '—'}h)</SettingLabel>
|
usedLabel={
|
||||||
<SettingValue>{promptsLabel}</SettingValue>
|
promptQuota === null
|
||||||
</SettingRow>
|
? `${promptsUsed.toLocaleString()} used`
|
||||||
<SettingRow>
|
: `${promptsUsed.toLocaleString()} / ${promptQuota.toLocaleString()}`
|
||||||
<SettingLabel>Period tokens</SettingLabel>
|
}
|
||||||
<SettingValue>
|
pct={promptPct}
|
||||||
in {formatTokenCount(usage?.tokens_in_period ?? null)} · out{' '}
|
/>
|
||||||
{formatTokenCount(usage?.tokens_out_period ?? null)}
|
|
||||||
</SettingValue>
|
{tokenQuota !== null ? (
|
||||||
</SettingRow>
|
<UsageMeter
|
||||||
|
label="Tokens this month"
|
||||||
|
meta={
|
||||||
|
usage?.period_end
|
||||||
|
? `Resets ${formatBillingDate(usage.period_end)}`
|
||||||
|
: 'Calendar month'
|
||||||
|
}
|
||||||
|
usedLabel={
|
||||||
|
tokensUsed === null
|
||||||
|
? `— / ${tokenQuota.toLocaleString()}`
|
||||||
|
: `${formatTokenCount(tokensUsed)} / ${tokenQuota.toLocaleString()}`
|
||||||
|
}
|
||||||
|
pct={tokenPct}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<UsageMeter
|
||||||
|
label="Tokens this period"
|
||||||
|
meta={
|
||||||
|
usage?.period_end
|
||||||
|
? `Period ends ${formatBillingDate(usage.period_end)}`
|
||||||
|
: 'This month'
|
||||||
|
}
|
||||||
|
usedLabel={`in ${formatTokenCount(usage?.tokens_in_period ?? null)} · out ${formatTokenCount(usage?.tokens_out_period ?? null)}`}
|
||||||
|
pct={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UpgradeHint>
|
||||||
|
Need more capacity? Manage or upgrade your plan in Billing below.
|
||||||
|
</UpgradeHint>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|||||||
Reference in New Issue
Block a user