← back to Watches
src/components/TrendIndicator.jsx
164 lines
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
/**
* TrendIndicator - Shows price trend with arrow and percentage
* @param {Object} trends - { '7d': number, '30d': number, '90d': number }
* @param {string} defaultPeriod - Initial period to show: '7d', '30d', '90d'
* @param {boolean} compact - Compact mode for cards
* @param {boolean} showPeriodSelector - Show period selection tabs
* @param {string} className - Additional CSS classes
*/
export default function TrendIndicator({
trends,
defaultPeriod = '30d',
compact = false,
showPeriodSelector = true,
className = ''
}) {
const [selectedPeriod, setSelectedPeriod] = useState(defaultPeriod);
const [showTooltip, setShowTooltip] = useState(false);
// Handle missing data
if (!trends) {
return (
<div className={`text-gray-500 text-sm ${className}`}>
<span>Trend unavailable</span>
</div>
);
}
const periods = ['7d', '30d', '90d'];
const periodLabels = { '7d': '7 days', '30d': '30 days', '90d': '90 days' };
const currentTrend = trends[selectedPeriod] ?? 0;
const isPositive = currentTrend > 0;
const isNegative = currentTrend < 0;
const isStable = currentTrend === 0;
// Determine colors and arrow
let arrowIcon, colorClasses, glowColor;
if (isPositive) {
arrowIcon = '↑';
colorClasses = 'text-green-400';
glowColor = 'rgba(34, 197, 94, 0.3)';
} else if (isNegative) {
arrowIcon = '↓';
colorClasses = 'text-red-400';
glowColor = 'rgba(239, 68, 68, 0.3)';
} else {
arrowIcon = '→';
colorClasses = 'text-gray-400';
glowColor = 'rgba(156, 163, 175, 0.3)';
}
const percentageDisplay = `${isPositive ? '+' : ''}${currentTrend.toFixed(1)}%`;
// Compact mode
if (compact) {
return (
<motion.div
className={`inline-flex items-center gap-1 ${colorClasses} ${className}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
title={`${periodLabels[selectedPeriod]} trend`}
>
<span className="text-sm font-medium">{arrowIcon}</span>
<span className="text-xs font-semibold">{percentageDisplay}</span>
</motion.div>
);
}
return (
<motion.div
className={`${className}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
{/* Period selector */}
{showPeriodSelector && (
<div className="flex gap-1 mb-3">
{periods.map((period) => (
<button
key={period}
onClick={() => setSelectedPeriod(period)}
className={`
px-2 py-1 text-xs rounded-md transition-all duration-200
${selectedPeriod === period
? 'bg-amber-500/20 text-amber-400 border border-amber-500/50'
: 'bg-gray-800/50 text-gray-400 border border-gray-700/50 hover:border-gray-600'
}
`}
>
{period}
</button>
))}
</div>
)}
{/* Trend display */}
<div
className="relative"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<motion.div
className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-800/50 border border-gray-700/50 ${colorClasses}`}
style={{ boxShadow: `0 0 20px ${glowColor}` }}
key={selectedPeriod}
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
>
<motion.span
className="text-2xl font-bold"
animate={{
y: isPositive ? [-2, 0] : isNegative ? [2, 0] : 0
}}
transition={{ repeat: Infinity, repeatType: 'reverse', duration: 1 }}
>
{arrowIcon}
</motion.span>
<div className="flex flex-col">
<span className="text-lg font-bold">{percentageDisplay}</span>
<span className="text-xs text-gray-500">{periodLabels[selectedPeriod]}</span>
</div>
</motion.div>
{/* Tooltip */}
<AnimatePresence>
{showTooltip && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
className="absolute bottom-full left-0 mb-2 px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg shadow-xl z-10 min-w-[200px]"
>
<p className="text-xs text-gray-400 mb-2">
Price change based on average market prices
</p>
<div className="space-y-1">
{periods.map((period) => {
const value = trends[period] ?? 0;
const isPos = value > 0;
const isNeg = value < 0;
return (
<div key={period} className="flex justify-between text-xs">
<span className="text-gray-500">{periodLabels[period]}:</span>
<span className={isPos ? 'text-green-400' : isNeg ? 'text-red-400' : 'text-gray-400'}>
{isPos ? '+' : ''}{value.toFixed(1)}%
</span>
</div>
);
})}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
}