← back to Watches

src/App.jsx

310 lines

import React, { useState, useEffect, useMemo } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { FiList, FiTrendingUp, FiAward, FiPieChart, FiArrowLeft, FiGrid, FiSearch } from 'react-icons/fi';
import HeroCarousel from './components/HeroCarousel';
import CollapsibleSection from './components/CollapsibleSection';
import WatchCard from './components/WatchCard';
import PriceChart from './components/PriceChart';
import { useDarkMode } from './hooks/useLocalStorage';
import { saveWatchesOffline, getWatchesOffline } from './utils/offlineStorage';
import { getOnlineStatus } from './utils/pwaUtils';

function App() {
  const [watches, setWatches] = useState([]);
  const [statistics, setStatistics] = useState(null);
  const [selectedWatch, setSelectedWatch] = useState(null);
  const [loading, setLoading] = useState(true);
  const [searchQuery, setSearchQuery] = useState('');
  const [darkMode] = useDarkMode();

  useEffect(() => {
    loadData();
  }, []);

  const loadData = async () => {
    try {
      setLoading(true);
      if (getOnlineStatus()) {
        const [watchesRes, statsRes] = await Promise.all([
          fetch('/api/watches'),
          fetch('/api/statistics')
        ]);
        const watchesData = await watchesRes.json();
        const statsData = await statsRes.json();
        const watchesList = watchesData.watches || watchesData;
        setWatches(watchesList);
        setStatistics(statsData);
        await saveWatchesOffline(watchesList);
      } else {
        const offlineWatches = await getWatchesOffline();
        setWatches(offlineWatches);
      }
    } catch (error) {
      console.error('Error loading data:', error);
      const offlineWatches = await getWatchesOffline();
      if (offlineWatches.length > 0) setWatches(offlineWatches);
    } finally {
      setLoading(false);
    }
  };

  // Calculate watch stats
  const watchesWithStats = useMemo(() => {
    return watches.map(watch => {
      const first = watch.priceHistory[0]?.price || 1;
      const last = watch.priceHistory[watch.priceHistory.length - 1]?.price || first;
      const appreciation = ((last - first) / first) * 100;
      return { ...watch, appreciation, currentPrice: last };
    });
  }, [watches]);

  // Top performers
  const topPerformers = useMemo(() => {
    return [...watchesWithStats]
      .sort((a, b) => b.appreciation - a.appreciation)
      .slice(0, 10);
  }, [watchesWithStats]);

  // Series breakdown
  const seriesBreakdown = useMemo(() => {
    const map = {};
    watches.forEach(w => {
      map[w.series] = (map[w.series] || 0) + 1;
    });
    return Object.entries(map)
      .map(([name, count]) => ({ name, count }))
      .sort((a, b) => b.count - a.count);
  }, [watches]);

  // Filtered watches
  const filteredWatches = useMemo(() => {
    if (!searchQuery.trim()) return watchesWithStats;
    const q = searchQuery.toLowerCase();
    return watchesWithStats.filter(w =>
      w.model.toLowerCase().includes(q) ||
      w.series.toLowerCase().includes(q) ||
      w.description?.toLowerCase().includes(q)
    );
  }, [watchesWithStats, searchQuery]);

  const handleWatchSelect = (watch) => {
    setSelectedWatch(watch);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const handleBack = () => {
    setSelectedWatch(null);
  };

  // Loading State
  if (loading) {
    return (
      <div className="min-h-screen flex items-center justify-center bg-slate-900">
        <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
          <div className="w-20 h-20 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center mx-auto mb-4 animate-pulse shadow-lg shadow-amber-500/30">
            <span className="text-3xl">Ω</span>
          </div>
          <h1 className="text-white text-xl font-bold">OMEGA VAULT</h1>
          <p className="text-amber-400 text-sm mt-1">Loading...</p>
        </motion.div>
      </div>
    );
  }

  // Detail View (when watch is selected)
  if (selectedWatch) {
    return (
      <div className="min-h-screen bg-slate-900">
        {/* Back Header */}
        <header className="fixed top-0 left-0 right-0 z-50 bg-slate-900/95 backdrop-blur-lg border-b border-slate-700">
          <div className="container mx-auto px-4 h-16 flex items-center">
            <button
              onClick={handleBack}
              className="flex items-center gap-2 text-amber-400 hover:text-amber-300 font-medium transition-colors"
            >
              <FiArrowLeft />
              <span>Back to Collection</span>
            </button>
          </div>
        </header>

        <main className="pt-20 pb-12">
          <div className="container mx-auto px-4">
            <PriceChart watch={selectedWatch} onBack={handleBack} />
          </div>
        </main>
      </div>
    );
  }

  // Main Single Page View
  return (
    <div className="min-h-screen bg-slate-900">
      {/* Fixed Header */}
      <header className="fixed top-0 left-0 right-0 z-50 bg-slate-900/90 backdrop-blur-lg border-b border-amber-500/20">
        <div className="container mx-auto px-4 h-16 flex items-center justify-between">
          <div className="flex items-center gap-3">
            <div className="w-10 h-10 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center shadow-lg shadow-amber-500/20">
              <span className="text-lg font-bold">Ω</span>
            </div>
            <div className="hidden sm:block">
              <h1 className="text-lg font-bold text-white">OMEGA <span className="text-amber-400">VAULT</span></h1>
            </div>
          </div>

          {/* Search */}
          <div className="flex-1 max-w-md mx-4">
            <div className="relative">
              <FiSearch className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
              <input
                type="text"
                placeholder="Search watches..."
                value={searchQuery}
                onChange={(e) => setSearchQuery(e.target.value)}
                className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-amber-500/50"
              />
            </div>
          </div>

          {/* Stats */}
          <div className="hidden md:flex items-center gap-4 text-sm">
            <span className="text-gray-400">{watches.length} Watches</span>
            <span className="text-green-400">Live</span>
          </div>
        </div>
      </header>

      {/* Hero Carousel */}
      <section className="pt-16">
        <HeroCarousel watches={watches} onWatchSelect={handleWatchSelect} />
      </section>

      {/* Main Content - Collapsible Sections */}
      <main className="container mx-auto px-4 py-8 space-y-4">

        {/* All Watches Section */}
        <CollapsibleSection
          title="Complete Collection"
          subtitle={`${filteredWatches.length} watches available`}
          icon={FiGrid}
          badge={searchQuery ? 'Filtered' : null}
          defaultOpen={true}
        >
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mt-4">
            {filteredWatches.map((watch, index) => (
              <WatchCard
                key={watch.id}
                watch={watch}
                index={index}
                onSelect={handleWatchSelect}
              />
            ))}
          </div>
          {filteredWatches.length === 0 && (
            <p className="text-center text-gray-400 py-8">No watches found matching "{searchQuery}"</p>
          )}
        </CollapsibleSection>

        {/* Top Performers */}
        <CollapsibleSection
          title="Top Performers"
          subtitle="Highest appreciation watches"
          icon={FiAward}
          badge="Top 10"
        >
          <div className="space-y-3 mt-4">
            {topPerformers.map((watch, index) => (
              <motion.div
                key={watch.id}
                initial={{ opacity: 0, x: -20 }}
                animate={{ opacity: 1, x: 0 }}
                transition={{ delay: index * 0.05 }}
                onClick={() => handleWatchSelect(watch)}
                className="flex items-center justify-between p-4 bg-slate-700/30 hover:bg-slate-700/50 rounded-xl cursor-pointer transition-colors group"
              >
                <div className="flex items-center gap-4">
                  <div className="w-10 h-10 rounded-full bg-gradient-to-br from-amber-500/20 to-amber-600/20 border border-amber-500/30 flex items-center justify-center text-amber-400 font-bold">
                    #{index + 1}
                  </div>
                  <div>
                    <h4 className="text-white font-semibold group-hover:text-amber-400 transition-colors">{watch.model}</h4>
                    <p className="text-gray-400 text-sm">{watch.series}</p>
                  </div>
                </div>
                <div className="text-right">
                  <p className="text-green-400 font-bold">+{watch.appreciation.toFixed(0)}%</p>
                  <p className="text-gray-400 text-sm">${watch.currentPrice.toLocaleString()}</p>
                </div>
              </motion.div>
            ))}
          </div>
        </CollapsibleSection>

        {/* Collection by Series */}
        <CollapsibleSection
          title="Collection Overview"
          subtitle="Watches by series"
          icon={FiPieChart}
        >
          <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 mt-4">
            {seriesBreakdown.map((series, index) => (
              <motion.div
                key={series.name}
                initial={{ opacity: 0, scale: 0.95 }}
                animate={{ opacity: 1, scale: 1 }}
                transition={{ delay: index * 0.05 }}
                className="p-4 bg-gradient-to-br from-slate-700/50 to-slate-800/50 border border-slate-600 rounded-xl hover:border-amber-500/50 transition-colors"
              >
                <h4 className="text-white font-semibold mb-1">{series.name}</h4>
                <p className="text-3xl font-bold text-amber-400">{series.count}</p>
                <p className="text-gray-400 text-xs">models</p>
              </motion.div>
            ))}
          </div>
        </CollapsibleSection>

        {/* Market Statistics */}
        {statistics && (
          <CollapsibleSection
            title="Market Statistics"
            subtitle="Overall market performance"
            icon={FiTrendingUp}
          >
            <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
              <div className="p-4 bg-slate-700/30 rounded-xl text-center">
                <p className="text-gray-400 text-sm mb-1">Total Watches</p>
                <p className="text-2xl font-bold text-white">{statistics.totalWatches}</p>
              </div>
              <div className="p-4 bg-slate-700/30 rounded-xl text-center">
                <p className="text-gray-400 text-sm mb-1">Watch Series</p>
                <p className="text-2xl font-bold text-white">{statistics.totalSeries}</p>
              </div>
              <div className="p-4 bg-slate-700/30 rounded-xl text-center">
                <p className="text-gray-400 text-sm mb-1">Avg Appreciation</p>
                <p className="text-2xl font-bold text-green-400">+{statistics.averageAppreciation}%</p>
              </div>
              <div className="p-4 bg-slate-700/30 rounded-xl text-center">
                <p className="text-gray-400 text-sm mb-1">Avg Price</p>
                <p className="text-2xl font-bold text-amber-400">
                  ${Math.round(watchesWithStats.reduce((sum, w) => sum + w.currentPrice, 0) / watchesWithStats.length).toLocaleString()}
                </p>
              </div>
            </div>
          </CollapsibleSection>
        )}

      </main>

      {/* Footer */}
      <footer className="border-t border-slate-700 py-8 mt-8">
        <div className="container mx-auto px-4 text-center text-gray-400 text-sm">
          <p>OMEGA VAULT - Price Intelligence Platform</p>
          <p className="mt-1">Tracking {watches.length} iconic timepieces since 1957</p>
        </div>
      </footer>
    </div>
  );
}

export default App;