mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-06-04 09:41:32 +02:00
feat: add Progress component using Radix UI and update dependencies
- Implemented a new Progress component in `components/ui/progress.tsx` utilizing Radix UI's progress primitives. - Added `@radix-ui/react-progress` as a dependency in `package.json`. - Updated `package-lock.json` to include new dependencies, including `recharts` and its related packages.
This commit is contained in:
@@ -1,120 +1,646 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useNostrEvents, useProfile } from "nostr-react";
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import NIP05 from '@/components/nip05';
|
||||
import { RecentFollowerCard } from './RecentFollowerCard';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
nip19,
|
||||
} from "nostr-tools";
|
||||
import { RecentZapsCard } from './RecentZapsCard';
|
||||
import {
|
||||
Users, Zap, Activity, TrendingUp, Calendar, CircleUser,
|
||||
MessageCircle, Share2, ThumbsUp,
|
||||
Network, LineChart, BarChart4, PieChart, Medal, Trophy,
|
||||
Heart, Gift, Sparkles
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ProfileInfoCardProps {
|
||||
pubkey: string;
|
||||
}
|
||||
|
||||
const ProfileInfoCard: React.FC<ProfileInfoCardProps> = ({ pubkey }) => {
|
||||
interface ZapStat {
|
||||
totalReceived: number;
|
||||
count: number;
|
||||
topZappers: {pubkey: string; amount: number}[];
|
||||
}
|
||||
|
||||
interface FollowStat {
|
||||
totalFollowers: number;
|
||||
totalFollowing: number;
|
||||
recentFollowers: any[];
|
||||
mutualFollows: number;
|
||||
}
|
||||
|
||||
interface NostrActivityStat {
|
||||
totalPosts: number;
|
||||
totalReplies: number;
|
||||
totalReactions: number;
|
||||
mostActiveMonth: string;
|
||||
postsPerDay: number;
|
||||
}
|
||||
|
||||
const NostrInsights: React.FC<ProfileInfoCardProps> = ({ pubkey }) => {
|
||||
const { data: userData, isLoading: userDataLoading } = useProfile({
|
||||
pubkey,
|
||||
});
|
||||
|
||||
const { events: followers, isLoading: followersLoading } = useNostrEvents({
|
||||
// Fetch followers (kind 3 events that include the user's pubkey)
|
||||
const { events: followerEvents, isLoading: followersLoading } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [3],
|
||||
'#p': [pubkey],
|
||||
},
|
||||
});
|
||||
|
||||
const { events: zaps, isLoading: zapsLoading } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [9735],
|
||||
'#p': [pubkey],
|
||||
limit: 50,
|
||||
},
|
||||
});
|
||||
|
||||
const { events: following, isLoading: followingLoading } = useNostrEvents({
|
||||
// Fetch user's follow list (kind 3 events authored by the user)
|
||||
const { events: followingEvents, isLoading: followingLoading } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [3],
|
||||
authors: [pubkey],
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// filter for only new followings (latest in a users followers list)
|
||||
const filteredFollowers = followers.filter(follower => {
|
||||
const lastPTag = follower.tags[follower.tags.length - 1];
|
||||
if (lastPTag[0] === "p" && lastPTag[1] === pubkey.toString()) {
|
||||
// console.log(follower.tags[follower.tags.length - 1]);
|
||||
return true;
|
||||
}
|
||||
// Fetch zaps (kind 9735 - zap receipts)
|
||||
const { events: zapEvents, isLoading: zapsLoading } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [9735],
|
||||
'#p': [pubkey],
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch user's posts (kind 1 - text notes)
|
||||
const { events: postEvents, isLoading: postsLoading } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 200,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch reactions to user's posts (kind 7 - reactions)
|
||||
const { events: reactionEvents, isLoading: reactionsLoading } = useNostrEvents({
|
||||
filter: {
|
||||
kinds: [7],
|
||||
'#p': [pubkey],
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate follower stats
|
||||
const followerStats: FollowStat = useMemo(() => {
|
||||
if (followersLoading || followingLoading) {
|
||||
return {
|
||||
totalFollowers: 0,
|
||||
totalFollowing: 0,
|
||||
recentFollowers: [],
|
||||
mutualFollows: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Get all pubkeys the user is following
|
||||
const followingPubkeys = followingEvents.length > 0
|
||||
? followingEvents[0]?.tags
|
||||
.filter(tag => tag[0] === 'p')
|
||||
.map(tag => tag[1])
|
||||
: [];
|
||||
|
||||
// Get all follower pubkeys
|
||||
const followerPubkeys = followerEvents.map(event => event.pubkey);
|
||||
|
||||
// Find mutual follows (intersection of followingPubkeys and followerPubkeys)
|
||||
const mutualFollows = followingPubkeys.filter(pk => followerPubkeys.includes(pk)).length;
|
||||
|
||||
// Filter for only recent followers (latest in a users followers list)
|
||||
const filteredFollowers = followerEvents.filter(follower => {
|
||||
const lastPTag = follower.tags[follower.tags.length - 1];
|
||||
if (lastPTag[0] === "p" && lastPTag[1] === pubkey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
totalFollowers: followerEvents.length,
|
||||
totalFollowing: followingPubkeys.length,
|
||||
recentFollowers: filteredFollowers.slice(-5).reverse(),
|
||||
mutualFollows
|
||||
};
|
||||
}, [followerEvents, followingEvents, followersLoading, followingLoading, pubkey]);
|
||||
|
||||
// Calculate zap stats
|
||||
const zapStats: ZapStat = useMemo(() => {
|
||||
if (zapsLoading) {
|
||||
return {
|
||||
totalReceived: 0,
|
||||
count: 0,
|
||||
topZappers: []
|
||||
};
|
||||
}
|
||||
|
||||
let totalSats = 0;
|
||||
const zapperMap = new Map<string, number>();
|
||||
|
||||
zapEvents.forEach(zap => {
|
||||
// Extract zap amount from bolt11 tag
|
||||
const bolt11Tag = zap.tags.find(tag => tag[0] === 'bolt11');
|
||||
if (!bolt11Tag || !bolt11Tag[1]) return;
|
||||
|
||||
// Extract zapper pubkey from P tag
|
||||
const zapperTag = zap.tags.find(tag => tag[0] === 'P');
|
||||
const zapperPubkey = zapperTag ? zapperTag[1] : zap.pubkey;
|
||||
|
||||
// Try to extract amount from description tag
|
||||
const descriptionTag = zap.tags.find(tag => tag[0] === 'description');
|
||||
if (descriptionTag && descriptionTag[1]) {
|
||||
try {
|
||||
const zapRequest = JSON.parse(descriptionTag[1]);
|
||||
const amountTag = zapRequest.tags.find((tag: string[]) => tag[0] === 'amount');
|
||||
if (amountTag && amountTag[1]) {
|
||||
const sats = parseInt(amountTag[1], 10) / 1000; // Convert msats to sats
|
||||
totalSats += sats;
|
||||
|
||||
// Track zapper amounts
|
||||
zapperMap.set(
|
||||
zapperPubkey,
|
||||
(zapperMap.get(zapperPubkey) || 0) + sats
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid JSON in description tag
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get top zappers
|
||||
const topZappers = Array.from(zapperMap.entries())
|
||||
.map(([pubkey, amount]) => ({ pubkey, amount }))
|
||||
.sort((a, b) => b.amount - a.amount)
|
||||
.slice(0, 5);
|
||||
|
||||
return {
|
||||
totalReceived: totalSats,
|
||||
count: zapEvents.length,
|
||||
topZappers
|
||||
};
|
||||
}, [zapEvents, zapsLoading]);
|
||||
|
||||
// Calculate post activity stats
|
||||
const activityStats: NostrActivityStat = useMemo(() => {
|
||||
if (postsLoading || reactionsLoading) {
|
||||
return {
|
||||
totalPosts: 0,
|
||||
totalReplies: 0,
|
||||
totalReactions: 0,
|
||||
mostActiveMonth: '',
|
||||
postsPerDay: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Count replies (posts with e tags)
|
||||
const replies = postEvents.filter(post =>
|
||||
post.tags.some(tag => tag[0] === 'e')
|
||||
);
|
||||
|
||||
// Count reactions received
|
||||
const reactionsReceived = reactionEvents.length;
|
||||
|
||||
// Calculate most active month
|
||||
const postsByMonth = postEvents.reduce((acc: Record<string, number>, post) => {
|
||||
const date = new Date(post.created_at * 1000);
|
||||
const monthYear = `${date.toLocaleString('default', { month: 'long' })} ${date.getFullYear()}`;
|
||||
acc[monthYear] = (acc[monthYear] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Find month with most posts
|
||||
let mostActiveMonth = '';
|
||||
let maxPosts = 0;
|
||||
Object.entries(postsByMonth).forEach(([month, count]) => {
|
||||
if (count > maxPosts) {
|
||||
mostActiveMonth = month;
|
||||
maxPosts = count;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate average posts per day (over last 30 days)
|
||||
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
const recentPosts = postEvents.filter(post => post.created_at * 1000 > thirtyDaysAgo);
|
||||
const postsPerDay = recentPosts.length / 30;
|
||||
|
||||
return {
|
||||
totalPosts: postEvents.length,
|
||||
totalReplies: replies.length,
|
||||
totalReactions: reactionsReceived,
|
||||
mostActiveMonth,
|
||||
postsPerDay
|
||||
};
|
||||
}, [postEvents, reactionEvents, postsLoading, reactionsLoading]);
|
||||
|
||||
// Format profile info
|
||||
let encoded = nip19.npubEncode(pubkey);
|
||||
let parts = encoded.split('npub');
|
||||
let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3);
|
||||
|
||||
const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened;
|
||||
const description = userData?.about?.replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||
const nip05 = userData?.nip05
|
||||
let profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey;
|
||||
const nip05 = userData?.nip05;
|
||||
const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey;
|
||||
|
||||
const isLoading = userDataLoading || followersLoading || followingLoading || zapsLoading || postsLoading || reactionsLoading;
|
||||
|
||||
// Calculate engagement score (a fun metric based on activity)
|
||||
const engagementScore = Math.min(100, Math.round(
|
||||
(followerStats.totalFollowers * 2) +
|
||||
(activityStats.totalPosts * 3) +
|
||||
(zapStats.totalReceived * 0.5) +
|
||||
(activityStats.totalReactions * 1.5)
|
||||
) / 10);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='pt-6 px-6'>
|
||||
{/* <ProfileInfoCard pubkey={pubkey.toString()} /> */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
{/* <CardTitle className="text-base font-normal">Profile</CardTitle> */}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Profile Card */}
|
||||
<Card className="mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-row items-center space-x-4">
|
||||
<Avatar>
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarImage
|
||||
src={profileImageSrc}
|
||||
alt="Avatar"
|
||||
className="rounded-full"
|
||||
/>
|
||||
<AvatarFallback>{title.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<h1 className="text-3xl font-bold">{title}</h1>
|
||||
<NIP05 nip05={nip05?.toString() ?? ''} pubkey={pubkey} />
|
||||
|
||||
<div className="flex items-center mt-2 space-x-2">
|
||||
<Medal className="h-4 w-4 text-yellow-500" />
|
||||
<span className="text-sm">Nostr Engagement: {engagementScore}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className='grid gap-4 grid-cols-2 p-6'>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-base font-normal">Total Followers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{followers.length}</div>
|
||||
{/* <p className="text-xs text-muted-foreground">
|
||||
+20.1% from last month
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-base font-normal">Total Following</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{followingLoading ? "Loading.." : (following.length > 0 ? following[0]?.tags.length : "-")}
|
||||
|
||||
{/* Tabs for different insights */}
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="grid grid-cols-3 mb-4">
|
||||
<TabsTrigger value="overview">
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="network">
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Network
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="zaps">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
Zaps
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Engagement Stats */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Engagement
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Engagement Score</span>
|
||||
<span className="font-medium">{engagementScore}%</span>
|
||||
</div>
|
||||
<Progress value={engagementScore} className="h-2" />
|
||||
|
||||
<div className="pt-4 space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="flex items-center">
|
||||
<MessageCircle className="mr-2 h-4 w-4 text-blue-500" />
|
||||
<span>{activityStats.totalPosts} posts</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Share2 className="mr-2 h-4 w-4 text-purple-500" />
|
||||
<span>{activityStats.totalReplies} replies</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<ThumbsUp className="mr-2 h-4 w-4 text-green-500" />
|
||||
<span>{activityStats.totalReactions} reactions</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Zap className="mr-2 h-4 w-4 text-yellow-500" />
|
||||
<span>{zapStats.count} zaps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Network Stats */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Network
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Followers</span>
|
||||
<span className="font-medium text-lg">{followerStats.totalFollowers}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Following</span>
|
||||
<span className="font-medium text-lg">{followerStats.totalFollowing}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Mutual Follows</span>
|
||||
<span className="font-medium text-lg">{followerStats.mutualFollows}</span>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className="text-xs text-muted-foreground">Follow Ratio</div>
|
||||
<Progress
|
||||
value={
|
||||
followerStats.totalFollowing > 0
|
||||
? Math.min(100, (followerStats.totalFollowers / followerStats.totalFollowing) * 50)
|
||||
: 0
|
||||
}
|
||||
className="h-2 mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Zap Stats */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
Zap Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Total Received</span>
|
||||
<span className="font-medium text-lg">{zapStats.totalReceived.toLocaleString()} sats</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Zap Count</span>
|
||||
<span className="font-medium text-lg">{zapStats.count}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm">Avg. per Zap</span>
|
||||
<span className="font-medium text-lg">
|
||||
{zapStats.count > 0
|
||||
? Math.round(zapStats.totalReceived / zapStats.count).toLocaleString()
|
||||
: 0} sats
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* <p className="text-xs text-muted-foreground">
|
||||
+20.1% from last month
|
||||
</p> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<RecentFollowerCard followers={filteredFollowers.reverse()} />
|
||||
<RecentZapsCard zaps={zaps.reverse() ?? []} />
|
||||
|
||||
{/* Activity Timeline */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
Activity Insights
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Stats based on {activityStats.totalPosts} posts and {activityStats.totalReactions} reactions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-muted-foreground">Most Active Time</div>
|
||||
<div className="font-semibold text-xl">{activityStats.mostActiveMonth || "No data"}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-muted-foreground">Avg. Posts per Day</div>
|
||||
<div className="font-semibold text-xl">{activityStats.postsPerDay.toFixed(1)}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-muted-foreground">Engagement Ratio</div>
|
||||
<div className="font-semibold text-xl">
|
||||
{activityStats.totalPosts > 0
|
||||
? (activityStats.totalReactions / activityStats.totalPosts).toFixed(1)
|
||||
: "0"} reactions/post
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Network Tab */}
|
||||
<TabsContent value="network" className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Recent Followers */}
|
||||
<RecentFollowerCard followers={followerStats.recentFollowers} />
|
||||
|
||||
{/* Network Stats Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Network className="mr-2 h-4 w-4" />
|
||||
Network Metrics
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">Mutual Follows Ratio</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-full">
|
||||
<Progress
|
||||
value={
|
||||
followerStats.totalFollowing > 0
|
||||
? (followerStats.mutualFollows / followerStats.totalFollowing) * 100
|
||||
: 0
|
||||
}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{followerStats.totalFollowing > 0
|
||||
? Math.round((followerStats.mutualFollows / followerStats.totalFollowing) * 100)
|
||||
: 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{followerStats.mutualFollows} out of {followerStats.totalFollowing} follows are mutual
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 space-y-1">
|
||||
<div className="text-sm font-medium">Network Stats</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Followers</span>
|
||||
<span>{followerStats.totalFollowers}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Following</span>
|
||||
<span>{followerStats.totalFollowing}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Ratio</span>
|
||||
<span>
|
||||
{followerStats.totalFollowing > 0
|
||||
? (followerStats.totalFollowers / followerStats.totalFollowing).toFixed(2)
|
||||
: "∞"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Mutuals</span>
|
||||
<span>{followerStats.mutualFollows}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Zaps Tab */}
|
||||
<TabsContent value="zaps" className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Zap Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
Zap Summary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div className="text-center py-4">
|
||||
<div className="text-3xl font-bold">{zapStats.totalReceived.toLocaleString()}</div>
|
||||
<div className="text-sm text-muted-foreground">Total Sats Received</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold">{zapStats.count}</div>
|
||||
<div className="text-xs text-muted-foreground">Total Zaps</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">
|
||||
{zapStats.count > 0
|
||||
? Math.round(zapStats.totalReceived / zapStats.count).toLocaleString()
|
||||
: 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Avg Sats/Zap</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold">
|
||||
{zapStats.topZappers.length > 0
|
||||
? zapStats.topZappers[0].amount.toLocaleString()
|
||||
: 0}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Largest Zap</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Zaps */}
|
||||
<RecentZapsCard zaps={zapEvents.slice(-5).reverse() ?? []} />
|
||||
</div>
|
||||
|
||||
{/* Top Zappers */}
|
||||
{zapStats.topZappers.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium flex items-center">
|
||||
<Trophy className="mr-2 h-4 w-4" />
|
||||
Top Supporters
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{zapStats.topZappers.map((zapper, index) => (
|
||||
<TopZapperItem
|
||||
key={zapper.pubkey}
|
||||
pubkey={zapper.pubkey}
|
||||
amount={zapper.amount}
|
||||
rank={index + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileInfoCard;
|
||||
// Component for displaying top zapper
|
||||
const TopZapperItem: React.FC<{pubkey: string; amount: number; rank: number}> = ({ pubkey, amount, rank }) => {
|
||||
const { data: userData } = useProfile({
|
||||
pubkey,
|
||||
});
|
||||
|
||||
// Format the pubkey for display
|
||||
let encoded = nip19.npubEncode(pubkey);
|
||||
let parts = encoded.split('npub');
|
||||
let npubShortened = 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3);
|
||||
|
||||
// Get user display name
|
||||
const name = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened;
|
||||
const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`flex items-center justify-center w-6 h-6 rounded-full
|
||||
${rank === 1 ? 'bg-yellow-100 text-yellow-700' :
|
||||
rank === 2 ? 'bg-gray-100 text-gray-700' :
|
||||
rank === 3 ? 'bg-amber-100 text-amber-700' : 'bg-blue-100 text-blue-700'}`}>
|
||||
<span className="text-xs font-bold">{rank}</span>
|
||||
</div>
|
||||
<Link href={`/profile/${encoded}`}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={profileImageSrc} alt={name} />
|
||||
<AvatarFallback>{name.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-sm font-medium">{name}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="font-semibold">{amount.toLocaleString()}</div>
|
||||
<div className="text-xs text-muted-foreground">sats</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NostrInsights;
|
||||
28
components/ui/progress.tsx
Normal file
28
components/ui/progress.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
323
package-lock.json
generated
323
package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-icons": "^5.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"recharts": "^2.15.3",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -4730,6 +4731,69 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
@@ -6026,9 +6090,129 @@
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
@@ -6059,6 +6243,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"dev": true,
|
||||
@@ -6238,6 +6428,16 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"license": "MIT"
|
||||
@@ -6841,6 +7041,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -6855,6 +7061,15 @@
|
||||
"version": "3.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
|
||||
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"license": "MIT",
|
||||
@@ -7444,6 +7659,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.2",
|
||||
"license": "MIT",
|
||||
@@ -9236,6 +9460,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-equals": "^5.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -9257,6 +9496,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
@@ -9287,6 +9542,44 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.3",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz",
|
||||
"integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
"eventemitter3": "^4.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react-is": "^18.3.1",
|
||||
"react-smooth": "^4.0.4",
|
||||
"recharts-scale": "^0.4.4",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"victory-vendor": "^36.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts-scale": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.4",
|
||||
"dev": true,
|
||||
@@ -10274,6 +10567,12 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -10600,6 +10899,28 @@
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
@@ -45,6 +46,7 @@
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-icons": "^5.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"recharts": "^2.15.3",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
Reference in New Issue
Block a user