# All files / src/pages Futures.tsx

0% Statements 0/209
0% Branches 0/1
0% Functions 0/1
0% Lines 0/209

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285

     import React, { useState, useEffect } from 'react';
    import { useDispatch } from 'react-redux';
    import { usePreferences } from '../contexts/PreferencesContext';
    import { useDarkMode } from '../contexts/DarkModeContext';
    import { formatOdds } from '../utils/format';
    import { addFutureLeg } from '../store/betSlipSlice';
    import apiClient from '../services/api';

    interface Outcome {
      outcome: string;
      bookmakers: Array<{
        bookmaker: string;
        price: number;
      }>;
      bestOdds: number;
    }

    interface Future {
      id: string;
      title: string;
      description?: string;
      status: string;
      sport: {
        key: string;
        name: string;
      };
      groupedOutcomes: Outcome[];
    }

    export default function Futures() {
      const { useDecimalOdds } = usePreferences();
      const { isDarkMode } = useDarkMode();
      const dispatch = useDispatch();
      const [futures, setFutures] = useState<Future[]>([]);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);
      const [selectedSport, setSelectedSport] = useState<string>('all');

      useEffect(() => {
        fetchFutures();
      }, [selectedSport]);

      const fetchFutures = async () => {
        try {
          setLoading(true);
          setError(null);

          const params = new URLSearchParams();
          if (selectedSport !== 'all') {
            params.append('sportKey', selectedSport);
          }

          const { data } = await apiClient.get(`/futures?${params}`);
          setFutures(data);
        } catch (err) {
          setError(err instanceof Error ? err.message : 'Unknown error');
        } finally {
          setLoading(false);
        }
      };

      const handleSync = async () => {
        try {
          await apiClient.post('/admin/sync-futures', {
            sportKey: selectedSport === 'all' ? null : selectedSport
          });
          setTimeout(() => fetchFutures(), 3000);
        } catch (err) {
          console.error('Sync failed:', err);
        }
      };

      const handleAddToBetSlip = (future: Future, outcome: Outcome) => {
        dispatch(addFutureLeg({
          futureId: future.id,
          futureTitle: future.title,
          outcome: outcome.outcome,
          odds: outcome.bestOdds,
          status: 'pending',
          sportKey: future.sport.key
        }));
      };

      if (loading) {
        return (
          <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
            <div className="text-center">
              <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
              <p className="text-gray-600 dark:text-gray-400">Loading futures...</p>
            </div>
          </div>
        );
      }

      return (
        <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 px-4 relative"
             style={{ imageRendering: 'pixelated' }}>
          {/* 8-bit Pixel Grid Background */}
          <div
            className="absolute inset-0 opacity-5 pointer-events-none"
            style={{
              backgroundImage: `repeating-linear-gradient(
                0deg,
                ${isDarkMode ? '#dc2626' : '#b91c1c'} 0px,
                transparent 2px,
                transparent 4px,
                ${isDarkMode ? '#dc2626' : '#b91c1c'} 4px
              ),
              repeating-linear-gradient(
                90deg,
                ${isDarkMode ? '#dc2626' : '#b91c1c'} 0px,
                transparent 2px,
                transparent 4px,
                ${isDarkMode ? '#dc2626' : '#b91c1c'} 4px
              )`,
              backgroundSize: '4px 4px',
              imageRendering: 'pixelated'
            }}
          />

          {/* CRT Scanlines Effect */}
          <div
            className="absolute inset-0 pointer-events-none opacity-3"
            style={{
              backgroundImage: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.3) 2px, rgba(0,0,0,0.3) 4px)',
              backgroundSize: '100% 4px'
            }}
          />

          <div className="max-w-7xl mx-auto relative z-10">
            {/* Header */}
            <div className="mb-8">
              <div className="flex items-center justify-between mb-4">
                <h1 className="text-3xl font-bold text-gray-900 dark:text-white"
                    style={{
                      fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
                      textShadow: '4px 4px 0px rgba(0,0,0,0.3)',
                      letterSpacing: '0.05em'
                    }}>
                  🏆 FUTURES BETTING
                </h1>
                <button
                  onClick={handleSync}
                  className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded transition-colors flex items-center gap-2 border-2 border-red-700"
                  style={{
                    fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
                    textShadow: '2px 2px 0px rgba(0,0,0,0.5)',
                    boxShadow: '4px 4px 0px rgba(0, 0, 0, 0.3)',
                    letterSpacing: '0.05em',
                    fontWeight: 'bold'
                  }}
                >
                  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
                  </svg>
                  Sync Odds
                </button>
              </div>
              <p className="text-gray-600 dark:text-gray-400 mb-4">
                Bet on championship winners, division titles, MVP awards, and more
              </p>

              {/* Sport Filter */}
              <div className="flex gap-2 flex-wrap">
                {['all', 'americanfootball_nfl', 'basketball_nba', 'icehockey_nhl', 'baseball_mlb'].map((sport) => (
                  <button
                    key={sport}
                    onClick={() => setSelectedSport(sport)}
                    className={`px-4 py-2 rounded font-medium transition-colors border-2 ${
                      selectedSport === sport
                        ? 'bg-red-600 text-white border-red-700'
                        : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700'
                    }`}
                    style={{
                      fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
                      textShadow: selectedSport === sport ? '2px 2px 0px rgba(0,0,0,0.5)' : 'none',
                      boxShadow: selectedSport === sport ? '4px 4px 0px rgba(0, 0, 0, 0.3)' : 'none',
                      letterSpacing: '0.05em'
                    }}
                  >
                    {sport === 'all' ? 'All Sports' : sport.split('_').pop()?.toUpperCase()}
                  </button>
                ))}
              </div>
            </div>

            {/* Error State */}
            {error && (
              <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6">
                <p className="text-red-800 dark:text-red-200">{error}</p>
              </div>
            )}

            {/* Empty State */}
            {!loading && futures.length === 0 && (
              <div className="bg-white dark:bg-gray-800 rounded-lg p-12 text-center">
                <div className="text-6xl mb-4">🏆</div>
                <h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
                  No Futures Available
                </h3>
                <p className="text-gray-600 dark:text-gray-400 mb-4">
                  Click "Sync Odds" to fetch the latest futures betting markets
                </p>
              </div>
            )}

            {/* Futures List */}
            <div className="space-y-6">
              {futures.map((future) => (
                <div key={future.id} className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden border-4 border-red-600 dark:border-red-700"
                     style={{
                       boxShadow: '8px 8px 0px rgba(0, 0, 0, 0.3)',
                       imageRendering: 'pixelated'
                     }}>
                  {/* Future Header */}
                  <div className="bg-gradient-to-r from-purple-600 to-blue-600 px-6 py-4">
                    <h2 className="text-xl font-bold text-white">{future.title}</h2>
                    <p className="text-purple-100 text-sm">{future.sport.name}</p>
                  </div>

                  {/* Outcomes Grid */}
                  <div className="p-6">
                    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
                      {future.groupedOutcomes
                        .sort((a, b) => b.bestOdds - a.bestOdds) // Sort by best odds (favorites first)
                        .map((outcome, idx) => (
                        <div
                          key={idx}
                          className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow"
                        >
                          <div className="flex items-start justify-between mb-3">
                            <h3 className="font-semibold text-gray-900 dark:text-white flex-1">
                              {outcome.outcome}
                            </h3>
                            <div className="ml-2">
                              <div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
                                {formatOdds(outcome.bestOdds, useDecimalOdds)}
                              </div>
                              <div className="text-xs text-gray-500 dark:text-gray-400 text-right">
                                Best Odds
                              </div>
                            </div>
                          </div>

                          {/* Bookmaker Odds */}
                          <div className="space-y-1 pt-3 border-t border-gray-100 dark:border-gray-700 mb-3">
                            {outcome.bookmakers.slice(0, 3).map((bookmaker, bidx) => (
                              <div key={bidx} className="flex justify-between text-sm">
                                <span className="text-gray-600 dark:text-gray-400 capitalize">
                                  {bookmaker.bookmaker.replace(/_/g, ' ')}
                                </span>
                                <span className="font-medium text-gray-900 dark:text-white">
                                  {formatOdds(bookmaker.price, useDecimalOdds)}
                                </span>
                              </div>
                            ))}
                            {outcome.bookmakers.length > 3 && (
                              <div className="text-xs text-gray-500 dark:text-gray-400 pt-1">
                                +{outcome.bookmakers.length - 3} more books
                              </div>
                            )}
                          </div>

                          {/* Add to Bet Slip Button */}
                          <button
                            onClick={() => handleAddToBetSlip(future, outcome)}
                            className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center justify-center gap-2 font-medium"
                          >
                            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
                            </svg>
                            Add to Bet Slip
                          </button>
                        </div>
                      ))}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      );
    }