← 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>
  );
}