fix: use Progress component for rank bars, consistent sats formatting

Replace custom rank bar divs with the existing Progress UI component
(with new indicatorClassName prop for color-coded fills).

Make sat amount displays consistent with zap receipt renderer pattern:
value and "sats" unit are rendered as separate elements — numeric value
in font-medium, unit in smaller muted text — matching how Kind9735
displays amounts across the app.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB
This commit is contained in:
Claude
2026-02-19 21:55:40 +00:00
parent 18dce794d9
commit 2c9010979a
3 changed files with 102 additions and 62 deletions

View File

@@ -17,40 +17,36 @@ import {
} from "@/lib/nip73-helpers";
import { formatTimestamp } from "@/hooks/useLocale";
import { ShieldCheck, User, FileText, Hash } from "lucide-react";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
function rankColor(rank: number) {
if (rank >= 70) return { indicator: "bg-green-600", text: "text-green-600" };
if (rank >= 40)
return { indicator: "bg-yellow-600", text: "text-yellow-600" };
return { indicator: "bg-red-600", text: "text-red-600" };
}
/**
* Color-coded rank bar with label
* Color-coded rank bar with label, using Progress component
*/
function RankBar({ rank }: { rank: number }) {
const clamped = Math.min(100, Math.max(0, rank));
const color =
clamped >= 70
? "bg-green-600"
: clamped >= 40
? "bg-yellow-600"
: "bg-red-600";
const textColor =
clamped >= 70
? "text-green-600"
: clamped >= 40
? "text-yellow-600"
: "text-red-600";
const { indicator, text } = rankColor(clamped);
return (
<div className="flex flex-col gap-1.5">
<span className="text-sm text-muted-foreground">Rank</span>
<div className="flex items-center gap-3">
<div className="h-2.5 flex-1 rounded-full bg-muted overflow-hidden">
<div
className={cn("h-full rounded-full transition-all", color)}
style={{ width: `${clamped}%` }}
/>
</div>
<Progress
value={clamped}
className="flex-1 bg-muted"
indicatorClassName={indicator}
/>
<span
className={cn(
"text-sm font-semibold tabular-nums w-12 text-right",
textColor,
text,
)}
>
{rank}/100
@@ -66,14 +62,23 @@ function RankBar({ rank }: { rank: number }) {
function MetricRow({
label,
value,
unit,
}: {
label: string;
value: string | number;
unit?: string;
}) {
return (
<div className="flex justify-between items-center py-1.5 border-b border-border/30 last:border-0">
<span className="text-sm text-muted-foreground">{label}</span>
<span className="text-sm font-medium tabular-nums">{value}</span>
<span className="text-sm font-medium tabular-nums">
{value}
{unit && (
<span className="text-xs font-normal text-muted-foreground ml-1">
{unit}
</span>
)}
</span>
</div>
);
}
@@ -149,8 +154,10 @@ function SubjectHeader({
function UserMetrics({ event }: { event: NostrEvent }) {
const data = getUserAssertionData(event);
type Metric = { label: string; value: string | number; unit?: string };
// Activity section
const activity: { label: string; value: string | number }[] = [];
const activity: Metric[] = [];
if (data.postCount !== undefined)
activity.push({ label: "Posts", value: data.postCount.toLocaleString() });
if (data.replyCount !== undefined)
@@ -180,16 +187,18 @@ function UserMetrics({ event }: { event: NostrEvent }) {
});
// Zaps section
const zaps: { label: string; value: string | number }[] = [];
const zaps: Metric[] = [];
if (data.zapAmountReceived !== undefined)
zaps.push({
label: "Received",
value: `${data.zapAmountReceived.toLocaleString()} sats`,
value: data.zapAmountReceived.toLocaleString(),
unit: "sats",
});
if (data.zapAmountSent !== undefined)
zaps.push({
label: "Sent",
value: `${data.zapAmountSent.toLocaleString()} sats`,
value: data.zapAmountSent.toLocaleString(),
unit: "sats",
});
if (data.zapCountReceived !== undefined)
zaps.push({
@@ -204,16 +213,18 @@ function UserMetrics({ event }: { event: NostrEvent }) {
if (data.zapAvgAmountDayReceived !== undefined)
zaps.push({
label: "Avg/Day In",
value: `${data.zapAvgAmountDayReceived.toLocaleString()} sats`,
value: data.zapAvgAmountDayReceived.toLocaleString(),
unit: "sats",
});
if (data.zapAvgAmountDaySent !== undefined)
zaps.push({
label: "Avg/Day Out",
value: `${data.zapAvgAmountDaySent.toLocaleString()} sats`,
value: data.zapAvgAmountDaySent.toLocaleString(),
unit: "sats",
});
// Moderation section
const moderation: { label: string; value: string | number }[] = [];
const moderation: Metric[] = [];
if (data.reportsReceived !== undefined)
moderation.push({
label: "Reports Received",
@@ -231,7 +242,12 @@ function UserMetrics({ event }: { event: NostrEvent }) {
<div className="flex flex-col">
<SectionHeader>Activity</SectionHeader>
{activity.map((m) => (
<MetricRow key={m.label} label={m.label} value={m.value} />
<MetricRow
key={m.label}
label={m.label}
value={m.value}
unit={m.unit}
/>
))}
</div>
)}
@@ -240,7 +256,12 @@ function UserMetrics({ event }: { event: NostrEvent }) {
<div className="flex flex-col">
<SectionHeader>Zaps</SectionHeader>
{zaps.map((m) => (
<MetricRow key={m.label} label={m.label} value={m.value} />
<MetricRow
key={m.label}
label={m.label}
value={m.value}
unit={m.unit}
/>
))}
</div>
)}
@@ -249,7 +270,12 @@ function UserMetrics({ event }: { event: NostrEvent }) {
<div className="flex flex-col">
<SectionHeader>Moderation</SectionHeader>
{moderation.map((m) => (
<MetricRow key={m.label} label={m.label} value={m.value} />
<MetricRow
key={m.label}
label={m.label}
value={m.value}
unit={m.unit}
/>
))}
</div>
)}
@@ -280,7 +306,8 @@ function UserMetrics({ event }: { event: NostrEvent }) {
function EventMetrics({ event }: { event: NostrEvent }) {
const data = getEventAssertionData(event);
const metrics: { label: string; value: string | number }[] = [];
const metrics: { label: string; value: string | number; unit?: string }[] =
[];
if (data.commentCount !== undefined)
metrics.push({
label: "Comments",
@@ -303,7 +330,8 @@ function EventMetrics({ event }: { event: NostrEvent }) {
if (data.zapAmount !== undefined)
metrics.push({
label: "Zap Amount",
value: `${data.zapAmount.toLocaleString()} sats`,
value: data.zapAmount.toLocaleString(),
unit: "sats",
});
if (metrics.length === 0) return null;
@@ -325,7 +353,8 @@ function ExternalMetrics({ event }: { event: NostrEvent }) {
const data = getExternalAssertionData(event);
const types = getExternalAssertionTypes(event);
const metrics: { label: string; value: string | number }[] = [];
const metrics: { label: string; value: string | number; unit?: string }[] =
[];
if (data.commentCount !== undefined)
metrics.push({
label: "Comments",
@@ -362,7 +391,12 @@ function ExternalMetrics({ event }: { event: NostrEvent }) {
<div className="flex flex-col">
<SectionHeader>Engagement</SectionHeader>
{metrics.map((m) => (
<MetricRow key={m.label} label={m.label} value={m.value} />
<MetricRow
key={m.label}
label={m.label}
value={m.value}
unit={m.unit}
/>
))}
</div>
)}

View File

@@ -16,35 +16,31 @@ import {
ASSERTION_KIND_LABELS,
ASSERTION_TAG_LABELS,
} from "@/lib/nip85-helpers";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
function rankColor(rank: number) {
if (rank >= 70) return { indicator: "bg-green-600", text: "text-green-600" };
if (rank >= 40)
return { indicator: "bg-yellow-600", text: "text-yellow-600" };
return { indicator: "bg-red-600", text: "text-red-600" };
}
/**
* Color-coded rank bar: green (>=70), yellow (40-69), red (<40)
* Color-coded rank bar using Progress component
*/
function RankBar({ rank }: { rank: number }) {
const clamped = Math.min(100, Math.max(0, rank));
const color =
clamped >= 70
? "bg-green-600"
: clamped >= 40
? "bg-yellow-600"
: "bg-red-600";
const textColor =
clamped >= 70
? "text-green-600"
: clamped >= 40
? "text-yellow-600"
: "text-red-600";
const { indicator, text } = rankColor(clamped);
return (
<div className="flex items-center gap-1.5">
<div className="h-2 w-32 rounded-full bg-muted overflow-hidden">
<div
className={cn("h-full rounded-full transition-all", color)}
style={{ width: `${clamped}%` }}
/>
</div>
<span className={cn("text-xs font-semibold tabular-nums", textColor)}>
<Progress
value={clamped}
className="w-32 bg-muted"
indicatorClassName={indicator}
/>
<span className={cn("text-xs font-semibold tabular-nums", text)}>
{rank}
</span>
</div>
@@ -126,7 +122,7 @@ function MetricsPreview({
const tags = getAssertionTags(event);
const rankTag = tags.find((t) => t.name === "rank");
let summaryMetrics: { label: string; value: string }[] = [];
let summaryMetrics: { label: string; value: string; unit?: string }[] = [];
if (event.kind === 30382) {
const data = getUserAssertionData(event);
@@ -143,7 +139,8 @@ function MetricsPreview({
if (data.zapAmountReceived !== undefined)
summaryMetrics.push({
label: "Zaps In",
value: `${data.zapAmountReceived.toLocaleString()} sats`,
value: data.zapAmountReceived.toLocaleString(),
unit: "sats",
});
} else if (event.kind === 30383 || event.kind === 30384) {
const data = getEventAssertionData(event);
@@ -160,7 +157,8 @@ function MetricsPreview({
if (data.zapAmount !== undefined)
summaryMetrics.push({
label: "Zaps",
value: `${data.zapAmount.toLocaleString()} sats`,
value: data.zapAmount.toLocaleString(),
unit: "sats",
});
} else if (event.kind === 30385) {
const data = getExternalAssertionData(event);
@@ -199,7 +197,10 @@ function MetricsPreview({
<div className="flex flex-wrap gap-x-4 gap-y-1">
{summaryMetrics.map((m) => (
<span key={m.label} className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">{m.value}</span>{" "}
<span className="font-medium text-foreground">{m.value}</span>
{m.unit && (
<span className="text-muted-foreground"> {m.unit}</span>
)}{" "}
{m.label}
</span>
))}

View File

@@ -5,8 +5,10 @@ import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
}
>(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
@@ -16,7 +18,10 @@ const Progress = React.forwardRef<
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
className={cn(
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>