← back to Handbag Auth Nextjs

src/components/PriceHistoryChart.tsx

340 lines

'use client'

import { LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
import { useState, useEffect } from 'react'

interface PriceHistoryChartProps {
  externalId?: string
  listingId?: string
  brand?: string
  model?: string
}

export function PriceHistoryChart({ externalId, listingId, brand, model }: PriceHistoryChartProps) {
  const [history, setHistory] = useState<any[]>([])
  const [loading, setLoading] = useState(true)
  const [chartType, setChartType] = useState<'line' | 'area'>('area')
  const [timeRange, setTimeRange] = useState<'7d' | '30d' | '90d' | 'all'>('all')

  useEffect(() => {
    fetchHistory()
  }, [externalId, listingId])

  const fetchHistory = async () => {
    try {
      const params = new URLSearchParams()
      if (externalId) params.append('externalId', externalId)
      if (listingId) params.append('listingId', listingId)
      params.append('limit', '1000')

      const res = await fetch(`/api/price-history?${params}`)
      const data = await res.json()

      if (data.success) {
        setHistory(data.history)
      }
    } catch (error) {
      console.error('Failed to fetch price history:', error)
    } finally {
      setLoading(false)
    }
  }

  const filterByTimeRange = (data: any[]) => {
    if (timeRange === 'all') return data

    const now = new Date()
    const days = timeRange === '7d' ? 7 : timeRange === '30d' ? 30 : 90
    const cutoffDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000)

    return data.filter(item => new Date(item.recordedAt) >= cutoffDate)
  }

  const exportToCSV = () => {
    const csv = [
      ['Date', 'Price USD', 'Price JPY', 'Available', 'Condition', 'Type'],
      ...history.map(h => [
        h.recordedAt,
        h.priceUsd || '',
        h.priceJpy,
        h.isAvailable ? 'Yes' : 'No',
        h.condition || '',
        h.listingType || ''
      ])
    ].map(row => row.join(',')).join('\n')

    const blob = new Blob([csv], { type: 'text/csv' })
    const url = window.URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `price-history-${externalId || listingId}-${new Date().toISOString().split('T')[0]}.csv`
    a.click()
    window.URL.revokeObjectURL(url)
  }

  const exportToJSON = () => {
    const json = JSON.stringify(history, null, 2)
    const blob = new Blob([json], { type: 'application/json' })
    const url = window.URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `price-history-${externalId || listingId}-${new Date().toISOString().split('T')[0]}.json`
    a.click()
    window.URL.revokeObjectURL(url)
  }

  if (loading) {
    return (
      <div className="bg-white rounded-xl shadow-lg p-6">
        <div className="animate-pulse">
          <div className="h-8 bg-gray-300 rounded w-1/3 mb-4"></div>
          <div className="h-64 bg-gray-300 rounded"></div>
        </div>
      </div>
    )
  }

  if (history.length === 0) {
    return (
      <div className="bg-white rounded-xl shadow-lg p-6">
        <h3 className="text-xl font-bold text-gray-900 mb-4">Price History</h3>
        <div className="text-center py-12">
          <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
          </svg>
          <p className="mt-2 text-sm text-gray-500">No price history data available yet</p>
        </div>
      </div>
    )
  }

  // Filter data by time range
  const filteredHistory = filterByTimeRange(history)

  // Calculate stats
  const prices = filteredHistory.map(h => h.priceUsd).filter(p => p)
  const currentPrice = prices[prices.length - 1]
  const originalPrice = prices[0]
  const highestPrice = Math.max(...prices)
  const lowestPrice = Math.min(...prices)
  const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length
  const priceChange = currentPrice - originalPrice
  const priceChangePercent = ((priceChange / originalPrice) * 100).toFixed(2)

  // Format data for chart
  const chartData = filteredHistory.map(item => ({
    date: new Date(item.recordedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
    fullDate: item.recordedAt,
    price: item.priceUsd || 0,
    priceJpy: item.priceJpy,
    available: item.isAvailable === 1,
  }))

  const CustomTooltip = ({ active, payload }: any) => {
    if (active && payload && payload.length) {
      const data = payload[0].payload
      return (
        <div className="bg-white p-4 rounded-lg shadow-xl border border-gray-200">
          <p className="text-sm text-gray-600 mb-1">{data.fullDate}</p>
          <p className="text-lg font-bold text-gray-900">${data.price.toLocaleString()}</p>
          <p className="text-sm text-gray-500">¥{data.priceJpy.toLocaleString()}</p>
          <p className={`text-xs mt-1 ${data.available ? 'text-green-600' : 'text-red-600'}`}>
            {data.available ? '✓ Available' : '✗ Sold/Unavailable'}
          </p>
        </div>
      )
    }
    return null
  }

  return (
    <div className="bg-white rounded-xl shadow-lg p-6">
      {/* Header */}
      <div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
        <div>
          <h3 className="text-2xl font-bold text-gray-900">Price History</h3>
          {brand && model && (
            <p className="text-sm text-gray-600 mt-1">{brand} {model}</p>
          )}
        </div>

        <div className="flex flex-wrap items-center gap-2">
          {/* Time Range Filter */}
          <div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
            {(['7d', '30d', '90d', 'all'] as const).map((range) => (
              <button
                key={range}
                onClick={() => setTimeRange(range)}
                className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${
                  timeRange === range
                    ? 'bg-white text-gray-900 shadow-sm'
                    : 'text-gray-600 hover:text-gray-900'
                }`}
              >
                {range === 'all' ? 'All Time' : range.toUpperCase()}
              </button>
            ))}
          </div>

          {/* Chart Type Toggle */}
          <div className="flex items-center space-x-1 bg-gray-100 rounded-lg p-1">
            <button
              onClick={() => setChartType('line')}
              className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${
                chartType === 'line'
                  ? 'bg-white text-gray-900 shadow-sm'
                  : 'text-gray-600 hover:text-gray-900'
              }`}
            >
              Line
            </button>
            <button
              onClick={() => setChartType('area')}
              className={`px-3 py-1 rounded-md text-xs font-medium transition-colors ${
                chartType === 'area'
                  ? 'bg-white text-gray-900 shadow-sm'
                  : 'text-gray-600 hover:text-gray-900'
              }`}
            >
              Area
            </button>
          </div>

          {/* Export Buttons */}
          <div className="relative group">
            <button className="px-3 py-1 bg-gray-100 text-gray-700 rounded-lg text-xs font-medium hover:bg-gray-200 transition-colors flex items-center space-x-1">
              <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
              </svg>
              <span>Export</span>
            </button>
            <div className="absolute right-0 mt-2 w-32 bg-white rounded-lg shadow-xl border border-gray-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-10">
              <button
                onClick={exportToCSV}
                className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 rounded-t-lg"
              >
                CSV
              </button>
              <button
                onClick={exportToJSON}
                className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 rounded-b-lg"
              >
                JSON
              </button>
            </div>
          </div>
        </div>
      </div>

      {/* Price Stats */}
      <div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
        <div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4">
          <p className="text-xs text-blue-600 font-medium uppercase tracking-wide">Current</p>
          <p className="text-2xl font-bold text-blue-900 mt-1">${currentPrice?.toLocaleString()}</p>
          <p className={`text-sm mt-1 ${priceChange >= 0 ? 'text-green-600' : 'text-red-600'}`}>
            {priceChange >= 0 ? '+' : ''}{priceChangePercent}%
          </p>
        </div>

        <div className="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4">
          <p className="text-xs text-green-600 font-medium uppercase tracking-wide">Lowest</p>
          <p className="text-2xl font-bold text-green-900 mt-1">${lowestPrice.toLocaleString()}</p>
          <p className="text-xs text-green-600 mt-1">Best deal</p>
        </div>

        <div className="bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg p-4">
          <p className="text-xs text-orange-600 font-medium uppercase tracking-wide">Average</p>
          <p className="text-2xl font-bold text-orange-900 mt-1">${Math.round(avgPrice).toLocaleString()}</p>
          <p className="text-xs text-orange-600 mt-1">Mean price</p>
        </div>

        <div className="bg-gradient-to-br from-red-50 to-red-100 rounded-lg p-4">
          <p className="text-xs text-red-600 font-medium uppercase tracking-wide">Highest</p>
          <p className="text-2xl font-bold text-red-900 mt-1">${highestPrice.toLocaleString()}</p>
          <p className="text-xs text-red-600 mt-1">Peak price</p>
        </div>

        <div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4">
          <p className="text-xs text-purple-600 font-medium uppercase tracking-wide">Data Points</p>
          <p className="text-2xl font-bold text-purple-900 mt-1">{filteredHistory.length}</p>
          <p className="text-xs text-purple-600 mt-1">Tracked prices</p>
        </div>
      </div>

      {/* Chart */}
      <div className="h-80 w-full">
        <ResponsiveContainer width="100%" height="100%">
          {chartType === 'area' ? (
            <AreaChart data={chartData}>
              <defs>
                <linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3}/>
                  <stop offset="95%" stopColor="#8b5cf6" stopOpacity={0}/>
                </linearGradient>
              </defs>
              <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
              <XAxis
                dataKey="date"
                stroke="#6b7280"
                style={{ fontSize: '12px' }}
              />
              <YAxis
                stroke="#6b7280"
                style={{ fontSize: '12px' }}
                tickFormatter={(value) => `$${value.toLocaleString()}`}
              />
              <Tooltip content={<CustomTooltip />} />
              <Area
                type="monotone"
                dataKey="price"
                stroke="#8b5cf6"
                strokeWidth={3}
                fill="url(#colorPrice)"
              />
            </AreaChart>
          ) : (
            <LineChart data={chartData}>
              <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
              <XAxis
                dataKey="date"
                stroke="#6b7280"
                style={{ fontSize: '12px' }}
              />
              <YAxis
                stroke="#6b7280"
                style={{ fontSize: '12px' }}
                tickFormatter={(value) => `$${value.toLocaleString()}`}
              />
              <Tooltip content={<CustomTooltip />} />
              <Line
                type="monotone"
                dataKey="price"
                stroke="#8b5cf6"
                strokeWidth={3}
                dot={{ fill: '#8b5cf6', r: 4 }}
                activeDot={{ r: 6 }}
              />
            </LineChart>
          )}
        </ResponsiveContainer>
      </div>

      {/* Legend */}
      <div className="mt-4 flex items-center justify-center space-x-6 text-sm text-gray-600">
        <div className="flex items-center">
          <div className="w-3 h-3 bg-purple-600 rounded-full mr-2"></div>
          <span>Price USD</span>
        </div>
        <div className="flex items-center">
          <div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
          <span>Available</span>
        </div>
        <div className="flex items-center">
          <div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
          <span>Sold</span>
        </div>
      </div>
    </div>
  )
}