mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 02:01:22 +02:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user