mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-13 00:46:54 +02:00
feat: add top monthly contributor highlight in user menu
- Add getTopMonthlyContributor() method to SupportersService to find the supporter with the highest donations in the current calendar month - Display top contributor below the progress bar in user menu with trophy icon, display name, and amount - Use useLiveQuery for reactive updates when new zaps arrive - Format amounts consistently (k/M notation for readability) - Only show when there is at least one contributor for the month
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
Zap,
|
||||
Trophy,
|
||||
} from "lucide-react";
|
||||
import accounts from "@/services/accounts";
|
||||
import { useProfile } from "@/hooks/useProfile";
|
||||
@@ -51,7 +52,7 @@ import {
|
||||
GRIMOIRE_DONATE_PUBKEY,
|
||||
GRIMOIRE_LIGHTNING_ADDRESS,
|
||||
} from "@/lib/grimoire-members";
|
||||
import { MONTHLY_GOAL_SATS } from "@/services/supporters";
|
||||
import supportersService, { MONTHLY_GOAL_SATS } from "@/services/supporters";
|
||||
|
||||
function UserAvatar({ pubkey }: { pubkey: string }) {
|
||||
const profile = useProfile(pubkey);
|
||||
@@ -82,6 +83,36 @@ function UserLabel({ pubkey }: { pubkey: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TopContributor({
|
||||
pubkey,
|
||||
amount,
|
||||
}: {
|
||||
pubkey: string;
|
||||
amount: number;
|
||||
}) {
|
||||
const profile = useProfile(pubkey);
|
||||
const displayName = getDisplayName(pubkey, profile);
|
||||
|
||||
function formatSats(sats: number): string {
|
||||
if (sats >= 1_000_000) {
|
||||
return `${(sats / 1_000_000).toFixed(1)}M`;
|
||||
} else if (sats >= 1_000) {
|
||||
return `${Math.floor(sats / 1_000)}k`;
|
||||
}
|
||||
return sats.toString();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-border/50">
|
||||
<Trophy className="size-3.5 text-yellow-500" />
|
||||
<span className="text-xs text-muted-foreground flex-1 truncate">
|
||||
{displayName}
|
||||
</span>
|
||||
<span className="text-xs font-medium">{formatSats(amount)} sats</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserMenu() {
|
||||
const account = use$(accounts.active$);
|
||||
const { state, addWindow, disconnectNWC, toggleWalletBalancesBlur } =
|
||||
@@ -109,6 +140,12 @@ export default function UserMenu() {
|
||||
return total;
|
||||
}, []) ?? 0;
|
||||
|
||||
// Get top monthly contributor reactively
|
||||
const topContributor = useLiveQuery(
|
||||
async () => supportersService.getTopMonthlyContributor(),
|
||||
[],
|
||||
);
|
||||
|
||||
// Calculate monthly donation progress
|
||||
const goalProgress = (monthlyDonations / MONTHLY_GOAL_SATS) * 100;
|
||||
|
||||
@@ -442,6 +479,12 @@ export default function UserMenu() {
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={goalProgress} className="h-1.5" />
|
||||
{topContributor && (
|
||||
<TopContributor
|
||||
pubkey={topContributor.pubkey}
|
||||
amount={topContributor.totalSats}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
|
||||
@@ -336,6 +336,47 @@ class SupportersService {
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top contributor for the current calendar month
|
||||
* Returns the supporter with the highest total zaps this month
|
||||
*/
|
||||
async getTopMonthlyContributor(): Promise<SupporterInfo | undefined> {
|
||||
const now = new Date();
|
||||
const firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const firstOfMonthTimestamp = Math.floor(firstOfMonth.getTime() / 1000);
|
||||
|
||||
const supporterMap = new Map<string, SupporterInfo>();
|
||||
|
||||
// Aggregate zaps by sender for current month
|
||||
await db.grimoireZaps
|
||||
.where("timestamp")
|
||||
.aboveOrEqual(firstOfMonthTimestamp)
|
||||
.each((zap) => {
|
||||
const existing = supporterMap.get(zap.senderPubkey);
|
||||
if (existing) {
|
||||
existing.totalSats += zap.amountSats;
|
||||
existing.zapCount += 1;
|
||||
existing.lastZapTimestamp = Math.max(
|
||||
existing.lastZapTimestamp,
|
||||
zap.timestamp,
|
||||
);
|
||||
} else {
|
||||
supporterMap.set(zap.senderPubkey, {
|
||||
pubkey: zap.senderPubkey,
|
||||
totalSats: zap.amountSats,
|
||||
zapCount: 1,
|
||||
lastZapTimestamp: zap.timestamp,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Find top contributor
|
||||
const supporters = Array.from(supporterMap.values());
|
||||
if (supporters.length === 0) return undefined;
|
||||
|
||||
return supporters.sort((a, b) => b.totalSats - a.totalSats)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supporter count using Dexie uniqueKeys
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user