diff --git a/bun.lockb b/bun.lockb index 8163ac5..9c6d047 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/dashboard/Statistics.tsx b/components/dashboard/Statistics.tsx index 132986f..413682c 100644 --- a/components/dashboard/Statistics.tsx +++ b/components/dashboard/Statistics.tsx @@ -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 = ({ 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 = ({ 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(); + + 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, 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, '
'); - 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 ( <>
- {/* */} - - - {/* Profile */} - - + {/* Profile Card */} + +
- + + {title.slice(0, 2)}
-

{title}

+

{title}

- +
+ + Nostr Engagement: {engagementScore}% +
-
-
- - - Total Followers - - -
{followers.length}
- {/*

- +20.1% from last month -

*/} -
-
- - - Total Following - - -
- {followingLoading ? "Loading.." : (following.length > 0 ? following[0]?.tags.length : "-")} + + {/* Tabs for different insights */} + + + + + Overview + + + + Network + + + + Zaps + + + + {/* Overview Tab */} + +
+ {/* Engagement Stats */} + + + + + Engagement + + + +
+
+ Engagement Score + {engagementScore}% +
+ + +
+
+
+ + {activityStats.totalPosts} posts +
+
+ + {activityStats.totalReplies} replies +
+
+ + {activityStats.totalReactions} reactions +
+
+ + {zapStats.count} zaps +
+
+
+
+
+
+ + {/* Network Stats */} + + + + + Network + + + +
+
+ Followers + {followerStats.totalFollowers} +
+
+ Following + {followerStats.totalFollowing} +
+
+ Mutual Follows + {followerStats.mutualFollows} +
+
+
Follow Ratio
+ 0 + ? Math.min(100, (followerStats.totalFollowers / followerStats.totalFollowing) * 50) + : 0 + } + className="h-2 mt-1" + /> +
+
+
+
+ + {/* Zap Stats */} + + + + + Zap Stats + + + +
+
+ Total Received + {zapStats.totalReceived.toLocaleString()} sats +
+
+ Zap Count + {zapStats.count} +
+
+ Avg. per Zap + + {zapStats.count > 0 + ? Math.round(zapStats.totalReceived / zapStats.count).toLocaleString() + : 0} sats + +
+
+
+
- {/*

- +20.1% from last month -

*/} - - - - + + {/* Activity Timeline */} + + + + + Activity Insights + + + Stats based on {activityStats.totalPosts} posts and {activityStats.totalReactions} reactions + + + +
+
+
Most Active Time
+
{activityStats.mostActiveMonth || "No data"}
+
+
+
Avg. Posts per Day
+
{activityStats.postsPerDay.toFixed(1)}
+
+
+
Engagement Ratio
+
+ {activityStats.totalPosts > 0 + ? (activityStats.totalReactions / activityStats.totalPosts).toFixed(1) + : "0"} reactions/post +
+
+
+
+
+
+ + {/* Network Tab */} + +
+ {/* Recent Followers */} + + + {/* Network Stats Card */} + + + + + Network Metrics + + + +
+
+
Mutual Follows Ratio
+
+
+ 0 + ? (followerStats.mutualFollows / followerStats.totalFollowing) * 100 + : 0 + } + className="h-2" + /> +
+
+ {followerStats.totalFollowing > 0 + ? Math.round((followerStats.mutualFollows / followerStats.totalFollowing) * 100) + : 0}% +
+
+
+ {followerStats.mutualFollows} out of {followerStats.totalFollowing} follows are mutual +
+
+ +
+
Network Stats
+
+
+ Followers + {followerStats.totalFollowers} +
+
+ Following + {followerStats.totalFollowing} +
+
+ Ratio + + {followerStats.totalFollowing > 0 + ? (followerStats.totalFollowers / followerStats.totalFollowing).toFixed(2) + : "∞"} + +
+
+ Mutuals + {followerStats.mutualFollows} +
+
+
+
+
+
+
+
+ + {/* Zaps Tab */} + +
+ {/* Zap Summary */} + + + + + Zap Summary + + + +
+
+
{zapStats.totalReceived.toLocaleString()}
+
Total Sats Received
+
+ +
+
+
{zapStats.count}
+
Total Zaps
+
+
+
+ {zapStats.count > 0 + ? Math.round(zapStats.totalReceived / zapStats.count).toLocaleString() + : 0} +
+
Avg Sats/Zap
+
+
+
+ {zapStats.topZappers.length > 0 + ? zapStats.topZappers[0].amount.toLocaleString() + : 0} +
+
Largest Zap
+
+
+
+
+
+ + {/* Recent Zaps */} + +
+ + {/* Top Zappers */} + {zapStats.topZappers.length > 0 && ( + + + + + Top Supporters + + + +
+ {zapStats.topZappers.map((zapper, index) => ( + + ))} +
+
+
+ )} +
+
); } -export default ProfileInfoCard; \ No newline at end of file +// 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 ( +
+
+
+ {rank} +
+ +
+ + + {name.slice(0, 2)} + +
{name}
+
+ +
+
+
{amount.toLocaleString()}
+
sats
+
+
+ ); +}; + +export default NostrInsights; \ No newline at end of file diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..5c87ea4 --- /dev/null +++ b/components/ui/progress.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/package-lock.json b/package-lock.json index d5ddbf6..24b1aea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c504ffc..e7aa125 100644 --- a/package.json +++ b/package.json @@ -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",