mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat(epic-2): implement WhaleActivityFeed component with mock data
- Add WhaleActivityFeed.tsx for displaying whale transactions (>0K) - Features: real-time feed, filter by watchlist/smart money, track wallet - Add mock whale transaction data with 7 sample transactions - Support for buy/sell indicators, smart money badges, explorer links - Implements Story 2.2: Whale Activity Tracker from Epic 2
This commit is contained in:
parent
e4d020799b
commit
db22cd4a64
2 changed files with 373 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import type { TokenData } from "../context/PageContextProvider";
|
|||
import type { WatchlistToken, WatchlistAlert } from "../crypto/WatchlistPanel";
|
||||
import type { SafetyFactor } from "../crypto/SafetyScoreDisplay";
|
||||
import type { AlertConfig } from "../crypto/AlertConfigModal";
|
||||
import type { WhaleTransaction } from "../whale/WhaleActivityFeed";
|
||||
|
||||
// ============================================
|
||||
// MOCK TOKEN DATA (DexScreener)
|
||||
|
|
@ -196,6 +197,111 @@ export const MOCK_WATCHLIST_ALERTS: WatchlistAlert[] = [
|
|||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// MOCK WHALE TRANSACTIONS
|
||||
// ============================================
|
||||
|
||||
export const MOCK_WHALE_TRANSACTIONS: WhaleTransaction[] = [
|
||||
{
|
||||
id: "whale-1",
|
||||
tokenSymbol: "BULLA",
|
||||
tokenName: "Bulla Token",
|
||||
chain: "solana",
|
||||
type: "buy",
|
||||
amountUSD: 100000,
|
||||
amountTokens: "8.1B",
|
||||
walletAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
|
||||
txHash: "5Kn8WqXZKqYqKqYqKqYqKqYqKqYqKqYqKqYqKqYqKqYq",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 2), // 2 mins ago
|
||||
isSmartMoney: true,
|
||||
isInWatchlist: true,
|
||||
},
|
||||
{
|
||||
id: "whale-2",
|
||||
tokenSymbol: "BONK",
|
||||
tokenName: "Bonk",
|
||||
chain: "solana",
|
||||
type: "sell",
|
||||
amountUSD: 50000,
|
||||
amountTokens: "2.3B",
|
||||
walletAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
|
||||
txHash: "3Hn7WpXZKpYpKpYpKpYpKpYpKpYpKpYpKpYpKpYpKpYp",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 5), // 5 mins ago
|
||||
isSmartMoney: false,
|
||||
isInWatchlist: true,
|
||||
},
|
||||
{
|
||||
id: "whale-3",
|
||||
tokenSymbol: "PEPE",
|
||||
tokenName: "Pepe",
|
||||
chain: "ethereum",
|
||||
type: "buy",
|
||||
amountUSD: 250000,
|
||||
amountTokens: "23B",
|
||||
walletAddress: "0x6982508145454Ce325dDbE47a25d4ec3d2311933",
|
||||
txHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 10), // 10 mins ago
|
||||
isSmartMoney: true,
|
||||
isInWatchlist: true,
|
||||
},
|
||||
{
|
||||
id: "whale-4",
|
||||
tokenSymbol: "WIF",
|
||||
tokenName: "dogwifhat",
|
||||
chain: "solana",
|
||||
type: "buy",
|
||||
amountUSD: 75000,
|
||||
amountTokens: "30.6K",
|
||||
walletAddress: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
|
||||
txHash: "4Jm6VoXYJoXoJoXoJoXoJoXoJoXoJoXoJoXoJoXoJoXo",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 15), // 15 mins ago
|
||||
isSmartMoney: false,
|
||||
isInWatchlist: true,
|
||||
},
|
||||
{
|
||||
id: "whale-5",
|
||||
tokenSymbol: "DEGEN",
|
||||
tokenName: "Degen",
|
||||
chain: "base",
|
||||
type: "sell",
|
||||
amountUSD: 35000,
|
||||
amountTokens: "2.2M",
|
||||
walletAddress: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
|
||||
txHash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 20), // 20 mins ago
|
||||
isSmartMoney: false,
|
||||
isInWatchlist: true,
|
||||
},
|
||||
{
|
||||
id: "whale-6",
|
||||
tokenSymbol: "SOL",
|
||||
tokenName: "Solana",
|
||||
chain: "solana",
|
||||
type: "buy",
|
||||
amountUSD: 500000,
|
||||
amountTokens: "5K",
|
||||
walletAddress: "So11111111111111111111111111111111111111112",
|
||||
txHash: "6Lp7XqYZLqZqLqZqLqZqLqZqLqZqLqZqLqZqLqZqLqZq",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 30), // 30 mins ago
|
||||
isSmartMoney: true,
|
||||
isInWatchlist: false,
|
||||
},
|
||||
{
|
||||
id: "whale-7",
|
||||
tokenSymbol: "MATIC",
|
||||
tokenName: "Polygon",
|
||||
chain: "ethereum",
|
||||
type: "buy",
|
||||
amountUSD: 150000,
|
||||
amountTokens: "200K",
|
||||
walletAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
|
||||
txHash: "0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321",
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 45), // 45 mins ago
|
||||
isSmartMoney: false,
|
||||
isInWatchlist: false,
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// MOCK ALERT CONFIGS
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,267 @@
|
|||
import { useState } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
ExternalLink,
|
||||
Filter,
|
||||
Star,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/routes/ui/button";
|
||||
import { ChainIcon } from "../components/shared/ChainIcon";
|
||||
|
||||
export interface WhaleTransaction {
|
||||
/** Transaction ID */
|
||||
id: string;
|
||||
/** Token symbol */
|
||||
tokenSymbol: string;
|
||||
/** Token name */
|
||||
tokenName?: string;
|
||||
/** Blockchain chain */
|
||||
chain: string;
|
||||
/** Transaction type */
|
||||
type: "buy" | "sell";
|
||||
/** Amount in USD */
|
||||
amountUSD: number;
|
||||
/** Amount in tokens */
|
||||
amountTokens: string;
|
||||
/** Wallet address */
|
||||
walletAddress: string;
|
||||
/** Transaction hash */
|
||||
txHash: string;
|
||||
/** When the transaction occurred */
|
||||
timestamp: Date;
|
||||
/** Whether this is a smart money wallet */
|
||||
isSmartMoney?: boolean;
|
||||
/** Whether this token is in user's watchlist */
|
||||
isInWatchlist?: boolean;
|
||||
}
|
||||
|
||||
export interface WhaleActivityFeedProps {
|
||||
/** List of whale transactions */
|
||||
transactions: WhaleTransaction[];
|
||||
/** Callback when transaction is clicked */
|
||||
onTransactionClick?: (tx: WhaleTransaction) => void;
|
||||
/** Callback when "Track Wallet" is clicked */
|
||||
onTrackWallet?: (walletAddress: string) => void;
|
||||
/** Callback when "View on Explorer" is clicked */
|
||||
onViewExplorer?: (txHash: string, chain: string) => void;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WhaleActivityFeed - Display whale transactions (large buys/sells >$10K)
|
||||
*
|
||||
* Features:
|
||||
* - Real-time feed of large transactions
|
||||
* - Filter by watchlist tokens or all tokens
|
||||
* - Smart money wallet indicators
|
||||
* - Quick links to block explorer
|
||||
* - Track wallet functionality
|
||||
*/
|
||||
export function WhaleActivityFeed({
|
||||
transactions,
|
||||
onTransactionClick,
|
||||
onTrackWallet,
|
||||
onViewExplorer,
|
||||
className,
|
||||
}: WhaleActivityFeedProps) {
|
||||
const [filter, setFilter] = useState<"all" | "watchlist" | "smart_money">("all");
|
||||
|
||||
// Filter transactions based on selected filter
|
||||
const filteredTransactions = transactions.filter((tx) => {
|
||||
if (filter === "watchlist") return tx.isInWatchlist;
|
||||
if (filter === "smart_money") return tx.isSmartMoney;
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatTimeAgo = (date: Date) => {
|
||||
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return `${Math.floor(hours / 24)}d ago`;
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number) => {
|
||||
if (amount >= 1000000) return `$${(amount / 1000000).toFixed(2)}M`;
|
||||
if (amount >= 1000) return `$${(amount / 1000).toFixed(1)}K`;
|
||||
return `$${amount.toFixed(0)}`;
|
||||
};
|
||||
|
||||
const shortenAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col h-full", className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-2xl">🐋</div>
|
||||
<div>
|
||||
<h2 className="font-semibold">Whale Activity</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{filteredTransactions.length} transactions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter tabs */}
|
||||
<div className="flex gap-1 p-2 border-b bg-muted/30">
|
||||
<Button
|
||||
variant={filter === "all" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setFilter("all")}
|
||||
className="flex-1"
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "watchlist" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setFilter("watchlist")}
|
||||
className="flex-1"
|
||||
>
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
Watchlist
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === "smart_money" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setFilter("smart_money")}
|
||||
className="flex-1"
|
||||
>
|
||||
Smart Money
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Transaction feed */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{filteredTransactions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
|
||||
<div className="text-4xl mb-2">🐋</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No whale activity detected yet
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Large transactions (>$10K) will appear here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{filteredTransactions.map((tx) => (
|
||||
<div
|
||||
key={tx.id}
|
||||
className="p-4 hover:bg-muted/50 transition-colors cursor-pointer"
|
||||
onClick={() => onTransactionClick?.(tx)}
|
||||
>
|
||||
{/* Time and smart money badge */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
{formatTimeAgo(tx.timestamp)}
|
||||
</div>
|
||||
{tx.isSmartMoney && (
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 bg-purple-500/10 text-purple-600 dark:text-purple-400 rounded-full text-xs font-medium">
|
||||
⚠️ Smart Money
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Transaction type and amount */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{tx.type === "buy" ? (
|
||||
<div className="flex items-center gap-1 text-green-600 dark:text-green-400 font-semibold">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
BUY
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-red-600 dark:text-red-400 font-semibold">
|
||||
<TrendingDown className="h-4 w-4" />
|
||||
SELL
|
||||
</div>
|
||||
)}
|
||||
<span className="font-bold text-lg">
|
||||
{formatAmount(tx.amountUSD)}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{tx.tokenSymbol}
|
||||
</span>
|
||||
<ChainIcon chain={tx.chain} size="sm" />
|
||||
</div>
|
||||
|
||||
{/* Token amount */}
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
Amount: {tx.amountTokens} tokens
|
||||
</div>
|
||||
|
||||
{/* Wallet address */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Wallet className="h-3 w-3 text-muted-foreground" />
|
||||
<code className="text-xs bg-muted px-2 py-0.5 rounded">
|
||||
{shortenAddress(tx.walletAddress)}
|
||||
</code>
|
||||
<button
|
||||
className="text-xs text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(tx.walletAddress);
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewExplorer?.(tx.txHash, tx.chain);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
Explorer
|
||||
</Button>
|
||||
{!tx.isSmartMoney && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTrackWallet?.(tx.walletAddress);
|
||||
}}
|
||||
>
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
Track Wallet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="border-t p-3 bg-muted/30">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Monitoring transactions >$10K</span>
|
||||
<span>Updates every 1 min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue