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:
2025-05-25 01:24:09 +02:00
parent a5d56dd5cc
commit 0a215ee98f
5 changed files with 938 additions and 61 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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;

View 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
View File

@@ -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",

View File

@@ -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",