diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx
index 7bd0fec..aadba05 100644
--- a/src/components/nostr/user-menu.tsx
+++ b/src/components/nostr/user-menu.tsx
@@ -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 (
+
+
+
+ {displayName}
+
+ {formatSats(amount)} sats
+
+ );
+}
+
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() {
+ {topContributor && (
+
+ )}
diff --git a/src/services/supporters.ts b/src/services/supporters.ts
index bee563c..88c3966 100644
--- a/src/services/supporters.ts
+++ b/src/services/supporters.ts
@@ -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 {
+ const now = new Date();
+ const firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
+ const firstOfMonthTimestamp = Math.floor(firstOfMonth.getTime() / 1000);
+
+ const supporterMap = new Map();
+
+ // 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
*/