npm package: @squadgolf/api-client
Description: TypeScript client library for Squad Golf API with intelligent caching and WebSocket support
Author: Squad Golf API
Latest version: 1.0.10

# Squad Golf API Client

A comprehensive TypeScript/JavaScript client library for the Squad Golf API with WebSocket support, intelligent caching, and comprehensive error handling.

[![npm version](https://badge.fury.io/js/@squadgolf%2Fapi-client.svg)](https://badge.fury.io/js/@squadgolf%2Fapi-client)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- 🏌️ **Complete Golf Data**: Tournaments, players, leaderboards, and rankings
- 🔄 **Real-time Updates**: WebSocket support for live leaderboard data with token authentication
- 🛡️ **Robust Authentication**: Automatic token management, refresh, and tier-based access control
- ⚡ **Smart Caching**: Intelligent caching with automatic invalidation and configurable TTL
- 🎯 **TypeScript Support**: Full type definitions with comprehensive IntelliSense
- 🌐 **Cross-platform**: Works in Node.js and browsers with environment-specific optimizations
- 📱 **React Hooks**: Built-in React hooks for seamless integration (coming soon)
- 🔄 **Auto-retry**: Circuit breaker pattern with exponential backoff and request deduplication
- 📦 **Tree-shakable**: Import only what you need for optimal bundle size

## Installation

```bash
npm install @squadgolf/api-client
```

## Quick Start

```typescript
import { createSquadGolfClient } from '@squadgolf/api-client';

const client = createSquadGolfClient({
  apiKey: 'your-api-key-here',
  baseUrl: 'https://api.squad.golf', // Optional, defaults to production
  enableWebSocket: true, // Enable real-time updates
});

// Get active tournaments
const tournaments = await client.tournaments.getAllTournaments({ status: 'active' });

// Get comprehensive tournament details (includes embedded leaderboard, field, etc.)
const tournament = await client.tournaments.getTournament('tournament-id');

// Subscribe to live leaderboard updates (async)
const unsubscribe = await client.tournaments.subscribeToLeaderboard(
  'tournament-id',
  (update) => {
    console.log('Live leaderboard update:', update.leaderboard.players);
  }
);

// Clean up subscription
unsubscribe();
```

## Authentication & Subscription Tiers

### Getting Your API Key

1. Visit [Squad Golf Dashboard](https://dashboard.squad.golf)
2. Sign up or log in to your account
3. Generate an API key from your dashboard
4. Choose your subscription tier:

### Subscription Tiers

- **🏌️ Driving Range** (Free): 250 requests/month
- **⛳ Front Nine** ($20/month): 5,000 requests/month + **WebSocket access**
- **🏆 Championship** ($50/month): 25,000 requests/month + webhooks + premium support
- **🥇 Masters** ($149/month): 100,000 requests/month + priority access + custom features

### WebSocket Authentication

WebSocket connections require a **Front Nine** subscription or higher. The client automatically handles:

- JWT token generation and refresh
- Connection management with 25-second heartbeats (optimized for 30-second server timeout)
- Automatic reconnection with exponential backoff
- Comprehensive error handling for tier restrictions

```typescript
const client = createSquadGolfClient({
  apiKey: 'your-api-key-here',
  enableWebSocket: true,
  websocket: {
    heartbeatInterval: 25000, // 25 seconds (recommended)
    autoReconnect: true,
    maxReconnectAttempts: 5,
    tokenRefreshThreshold: 120, // Refresh token 2 minutes before expiry
  }
});

// Connect to WebSocket
await client.connectWebSocket();

// Check connection status
console.log('WebSocket connected:', client.isWebSocketConnected());
```

## API Reference

### Tournaments

```typescript
// Get all tournaments with optional filtering
const tournaments = await client.tournaments.getAllTournaments({
  status: 'active' // 'upcoming', 'active', 'completed'
});

// Enhanced search with multiple filters
const searchResults = await client.tournaments.searchTournaments({
  query: 'Masters',
  status: ['upcoming', 'active'],
  year: '2024',
  location: 'Augusta',
  limit: 10
});

// Get comprehensive tournament details (includes ALL embedded data)
const tournament = await client.tournaments.getTournament('tournament-id');
// Returns: basic info + embedded leaderboard + field + overview + coverage + odds

// Get live leaderboard only (optimized for frequent updates)
const leaderboard = await client.tournaments.getLeaderboard('tournament-id');

// Get tournament statistics by status
const stats = await client.tournaments.getStats();
```

### Players

```typescript
// Get all players with pagination
const players = await client.players.getAllPlayers({
  limit: 50,
  page: 1
});

// Advanced player search with filtering
const playerSearch = await client.players.searchPlayers({
  name: 'Tiger Woods',
  country: 'USA',
  owgrRank: '1-50',
  fedexRank: '1-125',
  region: 'AMER',
  amateur: false,
  limit: 20
});

// Get complete player profile with current rankings
const player = await client.players.getPlayer('player-id');
```

### Rankings

```typescript
// Get current OWGR rankings
const owgrRankings = await client.rankings.getOWGRRankings({
  limit: 50
});

// Get FedEx Cup standings
const fedexRankings = await client.rankings.getFedExCupRankings({
  limit: 125
});
```

### Authentication Management

```typescript
// Generate additional API key
const newKey = await client.auth.generateApiKey({
  name: 'My Mobile App',
  permissions: ['read']
});

// Get current user information
const userInfo = await client.auth.getCurrentUser();

// Get API usage statistics
const usage = await client.auth.getUsageStats();

// Update user profile
await client.auth.updateProfile({
  name: 'Updated Name',
  company: 'My Company'
});

// Revoke an API key
await client.auth.revokeApiKey('key-id');
```

## Real-time WebSocket Updates

### Live Leaderboard Subscription

```typescript
const unsubscribe = await client.tournaments.subscribeToLeaderboard(
  'tournament-id',
  (update) => {
    console.log('Tournament:', update.tournamentId);
    console.log('Current round:', update.leaderboard.currentRound);
    console.log('Players:', update.leaderboard.players);
    
    // Update your UI with new leaderboard data
    updateLeaderboardUI(update.leaderboard);
  },
  {
    onError: (error) => {
      if (error.code === 'TIER_RESTRICTION') {
        console.log('WebSocket requires Front Nine subscription or higher');
        console.log('Current tier:', error.currentTier);
        console.log('Upgrade at:', error.upgradeUrl);
      }
    },
    onReconnect: () => {
      console.log('✅ Reconnected to live tournament updates');
    },
    onDisconnect: () => {
      console.log('❌ Disconnected from live updates');
    }
  }
);

// Unsubscribe when component unmounts or user navigates away
unsubscribe();
```

### WebSocket Error Handling

```typescript
import { WebSocketAuthError, WebSocketAuthErrorCode } from '@squadgolf/api-client';

await client.tournaments.subscribeToLeaderboard('tournament-id', updateHandler, {
  onError: (error) => {
    if (error instanceof WebSocketAuthError) {
      switch (error.code) {
        case WebSocketAuthErrorCode.TIER_RESTRICTION:
          // User needs to upgrade subscription
          showUpgradeDialog(error.upgradeUrl);
          break;
        case WebSocketAuthErrorCode.TOKEN_EXPIRED:
          // Token expired, client will automatically refresh
          console.log('Token expired, refreshing...');
          break;
        case WebSocketAuthErrorCode.AUTHENTICATION_REQUIRED:
          // Invalid API key
          showAuthenticationError();
          break;
        case WebSocketAuthErrorCode.INVALID_TOKEN:
          // Token is malformed
          console.error('Invalid WebSocket token');
          break;
        case WebSocketAuthErrorCode.INSUFFICIENT_PERMISSIONS:
          // API key lacks required permissions
          console.error('Insufficient permissions for WebSocket access');
          break;
      }
    }
  }
});
```

## Configuration Options

```typescript
const client = createSquadGolfClient({
  // Required
  apiKey: 'your-api-key',
  
  // Optional - API Configuration
  baseUrl: 'https://api.squad.golf', // Default: production API
  
  // Optional - HTTP Configuration
  http: {
    timeout: 10000, // Request timeout in milliseconds
    headers: {
      'User-Agent': 'MyApp/1.0.0',
      'X-Custom-Header': 'value'
    }
  },
  
  // Optional - Caching Configuration
  cache: {
    storage: 'memory', // 'memory' | 'localStorage' | 'sessionStorage'
    defaultTtl: 300, // Default cache TTL in seconds
    maxSize: 100 // Maximum cache entries
  },
  
  // Optional - Retry Configuration
  retry: {
    attempts: 3,
    delay: 1000,
    backoff: 'exponential' // 'linear' | 'exponential'
  },
  
  // Optional - Circuit Breaker Configuration
  circuitBreaker: {
    failureThreshold: 5, // Open circuit after N failures
    resetTimeout: 30000, // Try to close circuit after 30s
    monitoringPeriod: 10000 // Monitor failures over 10s
  },
  
  // Optional - WebSocket Configuration
  enableWebSocket: true,
  wsUrl: 'wss://api.squad.golf', // Default: production WebSocket
  websocket: {
    heartbeatInterval: 25000, // Heartbeat every 25 seconds (recommended)
    autoReconnect: true,
    maxReconnectAttempts: 5,
    reconnectDelay: 1000, // Initial reconnect delay
    maxReconnectDelay: 30000, // Maximum reconnect delay
    connectionTimeout: 10000, // Connection timeout
    tokenRefreshThreshold: 120 // Refresh token 2 minutes before expiry
  },
  
  // Optional - Logging Configuration
  logger: {
    enabled: false, // Enable/disable logging
    level: 'info', // 'debug' | 'info' | 'warn' | 'error'
    customLogger: (level, message, data) => {
      // Custom logging implementation
      console.log(`[${level.toUpperCase()}] ${message}`, data);
    }
  }
});
```

## Error Handling

### HTTP API Errors

```typescript
import { 
  ApiError, 
  AuthError, 
  ValidationError,
  CircuitBreakerError 
} from '@squadgolf/api-client';

try {
  const tournaments = await client.tournaments.getAllTournaments();
} catch (error) {
  if (error instanceof AuthError) {
    console.log('Authentication failed:', error.message);
    // Handle invalid API key or expired session
  } else if (error instanceof ValidationError) {
    console.log('Invalid request parameters:', error.details);
    // Handle validation errors from API
  } else if (error instanceof CircuitBreakerError) {
    console.log('Service temporarily unavailable:', error.message);
    // Handle rate limiting or service issues
  } else if (error instanceof ApiError) {
    console.log(`API error ${error.status}:`, error.message);
    // Handle other API errors
  } else {
    console.log('Unexpected error:', error);
  }
}
```

### WebSocket Errors

```typescript
import { WebSocketError, WebSocketAuthError } from '@squadgolf/api-client';

// Global WebSocket error handling
client.on('error', (error) => {
  if (error instanceof WebSocketAuthError) {
    console.log('WebSocket authentication error:', error.code);
  } else if (error instanceof WebSocketError) {
    console.log('WebSocket connection error:', error.message);
  }
});

// Connection state monitoring
client.on('websocketConnected', () => {
  console.log('✅ WebSocket connected');
});

client.on('websocketDisconnected', () => {
  console.log('❌ WebSocket disconnected');
});

client.on('websocketReconnecting', ({ attempt, delay }) => {
  console.log(`🔄 Reconnecting... (attempt ${attempt}, delay ${delay}ms)`);
});
```

## Performance & Caching

### Intelligent Caching

The client includes intelligent caching aligned with server-side TTL strategies:

```typescript
// Cache is automatically managed, but you can control it
client.clearCache(); // Clear all cached data

client.invalidateCache('tournament:*'); // Clear tournament cache patterns

client.invalidateCache('players:search:*'); // Clear player search cache

// Get comprehensive cache and client statistics
const stats = client.getStats();
console.log('HTTP stats:', stats.http);
console.log('Cache stats:', stats.cache);
console.log('WebSocket stats:', stats.websocket);

// Example cache stats
console.log('Cache hit ratio:', stats.cache.hitRatio);
console.log('Total requests:', stats.http.totalRequests);
console.log('Circuit breaker state:', stats.http.circuitBreaker.state);
```

### Rate Limiting & Circuit Breaker

The client automatically handles rate limiting and implements circuit breaker patterns:

```typescript
// Monitor circuit breaker state changes
client.on('circuitBreakerStateChange', (state) => {
  switch (state) {
    case 'CLOSED':
      console.log('✅ Circuit breaker closed - requests flowing normally');
      break;
    case 'OPEN':
      console.log('❌ Circuit breaker open - requests being blocked');
      break;
    case 'HALF_OPEN':
      console.log('🟡 Circuit breaker half-open - testing service recovery');
      break;
  }
});

// Rate limits are automatically handled based on your subscription tier
// The client will queue requests and retry with exponential backoff
```

## Client Health & Statistics

```typescript
// Get overall client health
const health = client.getHealthStatus();
console.log('Overall health:', health.overall); // 'healthy' | 'degraded' | 'unhealthy'
console.log('HTTP healthy:', health.http);
console.log('WebSocket healthy:', health.websocket);
console.log('Cache healthy:', health.cache);

// Get detailed performance statistics
const stats = client.getStats();
console.log('HTTP requests:', stats.http.totalRequests);
console.log('Cache hit ratio:', stats.cache.hitRatio);
console.log('WebSocket uptime:', stats.websocket?.uptime);

// Batch operations for efficiency
const results = await client.batch([
  () => client.tournaments.getAllTournaments({ status: 'active' }),
  () => client.players.searchPlayers({ name: 'Tiger' }),
  () => client.rankings.getOWGRRankings({ limit: 10 })
]);

console.log('Batch results:', results);
```

## TypeScript Support

The client is written in TypeScript and includes comprehensive type definitions:

```typescript
import type { 
  Tournament, 
  TournamentSummary,
  Player, 
  Leaderboard,
  LeaderboardUpdate,
  WebSocketMessage,
  WebSocketAuthError,
  TournamentSearchParams,
  PlayerSearchParams
} from '@squadgolf/api-client';

// All API responses are fully typed with IntelliSense support
const tournament: Tournament = await client.tournaments.getTournament('id');
const players: Player[] = tournament.field.players;
const leaderboard: Leaderboard = tournament.leaderboard;

// WebSocket updates are also fully typed
const unsubscribe = client.tournaments.subscribeToLeaderboard(
  'tournament-id',
  (update: LeaderboardUpdate) => {
    // Full type safety and IntelliSense
    const currentRound: number = update.leaderboard.currentRound;
    const players = update.leaderboard.players; // Player[]
  }
);
```

## Examples

### Basic Tournament Display

```typescript
import { createSquadGolfClient } from '@squadgolf/api-client';

async function displayActiveTournaments() {
  const client = createSquadGolfClient({
    apiKey: process.env.SQUAD_GOLF_API_KEY
  });

  try {
    const tournaments = await client.tournaments.getAllTournaments({ 
      status: 'active' 
    });

    tournaments.forEach(tournament => {
      console.log(`${tournament.name} - ${tournament.status}`);
      console.log(`Location: ${tournament.courseName}`);
      console.log(`Dates: ${tournament.startDate} to ${tournament.endDate}`);
    });
  } catch (error) {
    console.error('Failed to fetch tournaments:', error);
  }
}
```

### Live Leaderboard with WebSocket

```typescript
import { createSquadGolfClient } from '@squadgolf/api-client';

async function watchLiveLeaderboard(tournamentId: string) {
  const client = createSquadGolfClient({
    apiKey: process.env.SQUAD_GOLF_API_KEY,
    enableWebSocket: true
  });

  // Connect to WebSocket
  await client.connectWebSocket();

  // Subscribe to live updates
  const unsubscribe = await client.tournaments.subscribeToLeaderboard(
    tournamentId,
    (update) => {
      console.clear();
      console.log(`📺 LIVE: ${update.tournamentId}`);
      console.log(`Round ${update.leaderboard.currentRound}`);
      console.log('─'.repeat(50));
      
      update.leaderboard.players.slice(0, 10).forEach((player, index) => {
        const position = player.position.padEnd(4);
        const name = `${player.firstName} ${player.lastName}`.padEnd(20);
        const score = player.toPar > 0 ? `+${player.toPar}` : `${player.toPar}`;
        
        console.log(`${position} ${name} ${score}`);
      });
    },
    {
      onError: (error) => {
        console.error('WebSocket error:', error);
      },
      onReconnect: () => {
        console.log('🔄 Reconnected to live updates');
      }
    }
  );

  // Clean up on exit
  process.on('SIGINT', () => {
    unsubscribe();
    client.disconnectWebSocket();
    process.exit(0);
  });
}

// Usage
watchLiveLeaderboard('your-tournament-id');
```

### Player Search and Analysis

```typescript
async function analyzePlayer(playerName: string) {
  const client = createSquadGolfClient({
    apiKey: process.env.SQUAD_GOLF_API_KEY
  });

  // Search for player
  const searchResults = await client.players.searchPlayers({
    name: playerName,
    limit: 5
  });

  if (searchResults.length === 0) {
    console.log(`No players found matching "${playerName}"`);
    return;
  }

  // Get detailed info for the first match
  const player = await client.players.getPlayer(searchResults[0].id);
  
  console.log(`📋 Player Analysis: ${player.firstName} ${player.lastName}`);
  console.log(`Country: ${player.country}`);
  console.log(`OWGR Rank: ${player.owgrRank || 'Unranked'}`);
  console.log(`FedEx Cup Rank: ${player.fedexRank || 'Unranked'}`);
  
  if (player.currentForm) {
    console.log(`Recent Form: ${player.currentForm}`);
  }
}
```

### Next.js Real-time Leaderboard

Here's a complete example of building a live leaderboard component in Next.js with WebSocket support:

```typescript
// lib/golf-client.ts
import { createSquadGolfClient } from '@squadgolf/api-client';

export const golfClient = createSquadGolfClient({
  apiKey: process.env.NEXT_PUBLIC_SQUAD_GOLF_API_KEY!,
  enableWebSocket: true,
  websocket: {
    heartbeatInterval: 25000,
    autoReconnect: true,
    maxReconnectAttempts: 5,
  }
});
```

```typescript
// hooks/useLiveLeaderboard.ts
import { useState, useEffect, useCallback } from 'react';
import { golfClient } from '../lib/golf-client';
import type { Leaderboard, LeaderboardUpdate } from '@squadgolf/api-client';

interface UseLiveLeaderboardResult {
  leaderboard: Leaderboard | null;
  loading: boolean;
  error: string | null;
  isLive: boolean;
  connectionStatus: 'connected' | 'connecting' | 'disconnected' | 'error';
  lastUpdated: string | null;
}

export function useLiveLeaderboard(tournamentId: string): UseLiveLeaderboardResult {
  const [leaderboard, setLeaderboard] = useState<Leaderboard | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [isLive, setIsLive] = useState(false);
  const [connectionStatus, setConnectionStatus] = useState<'connected' | 'connecting' | 'disconnected' | 'error'>('disconnected');
  const [lastUpdated, setLastUpdated] = useState<string | null>(null);

  // Load initial leaderboard data
  useEffect(() => {
    let mounted = true;

    async function loadInitialData() {
      try {
        setLoading(true);
        setError(null);
        
        const initialLeaderboard = await golfClient.tournaments.getLeaderboard(tournamentId);
        
        if (mounted) {
          setLeaderboard(initialLeaderboard);
          setLastUpdated(initialLeaderboard.lastUpdated);
        }
      } catch (err) {
        if (mounted) {
          setError(err instanceof Error ? err.message : 'Failed to load leaderboard');
        }
      } finally {
        if (mounted) {
          setLoading(false);
        }
      }
    }

    loadInitialData();

    return () => {
      mounted = false;
    };
  }, [tournamentId]);

  // Handle WebSocket connection and subscription
  useEffect(() => {
    let unsubscribe: (() => void) | null = null;

    async function setupWebSocket() {
      try {
        setConnectionStatus('connecting');
        
        // Connect to WebSocket
        await golfClient.connectWebSocket();
        
        // Subscribe to live updates
        unsubscribe = await golfClient.tournaments.subscribeToLeaderboard(
          tournamentId,
          (update: LeaderboardUpdate) => {
            setLeaderboard(update.leaderboard);
            setIsLive(true);
            setConnectionStatus('connected');
            setError(null);
          },
          {
            onError: (error) => {
              setConnectionStatus('error');
              
              if (error.code === 'TIER_RESTRICTION') {
                setError('Live updates require a Front Nine subscription or higher. Upgrade at: ' + error.upgradeUrl);
                setIsLive(false);
              } else if (error.code === 'AUTHENTICATION_REQUIRED') {
                setError('Invalid API key. Please check your credentials.');
              } else {
                setError('WebSocket error: ' + error.message);
              }
            },
            onReconnect: () => {
              setConnectionStatus('connected');
              setError(null);
            },
            onDisconnect: () => {
              setConnectionStatus('disconnected');
              setIsLive(false);
            }
          }
        );

        setConnectionStatus('connected');
        setIsLive(true);
        
      } catch (err) {
        setConnectionStatus('error');
        setError(err instanceof Error ? err.message : 'Failed to connect to live updates');
        setIsLive(false);
      }
    }

    // Only setup WebSocket if we have initial data
    if (leaderboard && !loading) {
      setupWebSocket();
    }

    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, [tournamentId, leaderboard, loading]);

  return {
    leaderboard,
    loading,
    error,
    isLive,
    connectionStatus,
    lastUpdated
  };
}
```

```tsx
// components/LiveLeaderboard.tsx
import { useLiveLeaderboard } from '../hooks/useLiveLeaderboard';
import { formatDistanceToNow } from 'date-fns';

interface LiveLeaderboardProps {
  tournamentId: string;
}

export function LiveLeaderboard({ tournamentId }: LiveLeaderboardProps) {
  const { 
    leaderboard, 
    loading, 
    error, 
    isLive, 
    connectionStatus, 
    lastUpdated 
  } = useLiveLeaderboard(tournamentId);

  if (loading) {
    return (
      <div className="flex items-center justify-center p-8">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
        <span className="ml-2 text-gray-600">Loading leaderboard...</span>
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-red-50 border border-red-200 rounded-md p-4">
        <div className="flex">
          <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
            <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
          </svg>
          <div className="ml-3">
            <h3 className="text-sm font-medium text-red-800">Error loading leaderboard</h3>
            <p className="mt-1 text-sm text-red-700">{error}</p>
          </div>
        </div>
      </div>
    );
  }

  if (!leaderboard) {
    return (
      <div className="text-center p-8 text-gray-500">
        No leaderboard data available
      </div>
    );
  }

  return (
    <div className="bg-white shadow rounded-lg overflow-hidden">
      {/* Header with live status */}
      <div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
        <div className="flex items-center justify-between">
          <h2 className="text-lg font-semibold text-gray-900">
            Round {leaderboard.currentRound} Leaderboard
          </h2>
          <div className="flex items-center space-x-4">
            {isLive && (
              <div className="flex items-center">
                <div className="w-2 h-2 bg-red-500 rounded-full animate-pulse mr-2"></div>
                <span className="text-sm font-medium text-red-600">LIVE</span>
              </div>
            )}
            <div className={`px-2 py-1 rounded-full text-xs font-medium ${
              connectionStatus === 'connected' 
                ? 'bg-green-100 text-green-800' 
                : connectionStatus === 'connecting'
                ? 'bg-yellow-100 text-yellow-800'
                : connectionStatus === 'error'
                ? 'bg-red-100 text-red-800'
                : 'bg-gray-100 text-gray-800'
            }`}>
              {connectionStatus.toUpperCase()}
            </div>
          </div>
        </div>
        {lastUpdated && (
          <p className="text-sm text-gray-600 mt-1">
            Last updated: {formatDistanceToNow(new Date(lastUpdated), { addSuffix: true })}
          </p>
        )}
      </div>

      {/* Leaderboard table */}
      <div className="overflow-x-auto">
        <table className="min-w-full divide-y divide-gray-200">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Pos
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Player
              </th>
              <th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
                To Par
              </th>
              <th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
                Total
              </th>
              <th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
                Status
              </th>
            </tr>
          </thead>
          <tbody className="bg-white divide-y divide-gray-200">
            {leaderboard.players.map((player, index) => (
              <tr 
                key={player.playerId}
                className={`${
                  index < 3 ? 'bg-yellow-50' : ''
                } hover:bg-gray-50 transition-colors duration-200`}
              >
                <td className="px-6 py-4 whitespace-nowrap">
                  <div className={`text-sm font-medium ${
                    index === 0 ? 'text-yellow-600' : 
                    index === 1 ? 'text-gray-500' : 
                    index === 2 ? 'text-yellow-700' : 'text-gray-900'
                  }`}>
                    {player.position}
                    {index === 0 && ' 🏆'}
                  </div>
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <div className="text-sm font-medium text-gray-900">
                    {player.firstName} {player.lastName}
                  </div>
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-center">
                  <span className={`text-sm font-semibold ${
                    player.toPar < 0 ? 'text-red-600' :
                    player.toPar > 0 ? 'text-green-600' : 'text-gray-900'
                  }`}>
                    {player.toPar === 0 ? 'E' : 
                     player.toPar > 0 ? `+${player.toPar}` : 
                     player.toPar}
                  </span>
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-900">
                  {player.totalScore}
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-center">
                  <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
                    player.status === 'active' ? 'bg-green-100 text-green-800' :
                    player.status === 'finished' ? 'bg-blue-100 text-blue-800' :
                    'bg-gray-100 text-gray-800'
                  }`}>
                    {player.status}
                  </span>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Footer */}
      <div className="bg-gray-50 px-6 py-3 border-t border-gray-200">
        <div className="flex items-center justify-between text-sm text-gray-600">
          <span>Showing {leaderboard.players.length} players</span>
          {!isLive && (
            <span className="text-yellow-600">
              ⚠️ Live updates unavailable - showing cached data
            </span>
          )}
        </div>
      </div>
    </div>
  );
}
```

```tsx
// pages/tournament/[id].tsx
import { GetServerSideProps } from 'next';
import { useState, useEffect } from 'react';
import { golfClient } from '../../lib/golf-client';
import { LiveLeaderboard } from '../../components/LiveLeaderboard';
import type { Tournament } from '@squadgolf/api-client';

interface TournamentPageProps {
  tournament: Tournament;
  tournamentId: string;
}

export default function TournamentPage({ tournament: initialTournament, tournamentId }: TournamentPageProps) {
  const [tournament, setTournament] = useState<Tournament>(initialTournament);

  return (
    <div className="min-h-screen bg-gray-100">
      <div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
        {/* Tournament header */}
        <div className="bg-white shadow rounded-lg mb-6">
          <div className="px-6 py-4">
            <h1 className="text-2xl font-bold text-gray-900">
              {tournament.name}
            </h1>
            <div className="mt-2 flex items-center space-x-4 text-sm text-gray-600">
              <span>📅 {tournament.startDate} - {tournament.endDate}</span>
              <span>📍 {tournament.courseName}</span>
              <span className={`px-2 py-1 rounded-full text-xs font-medium ${
                tournament.status === 'active' ? 'bg-green-100 text-green-800' :
                tournament.status === 'upcoming' ? 'bg-blue-100 text-blue-800' :
                'bg-gray-100 text-gray-800'
              }`}>
                {tournament.status.toUpperCase()}
              </span>
            </div>
          </div>
        </div>

        {/* Live leaderboard */}
        <LiveLeaderboard tournamentId={tournamentId} />
      </div>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.params!;

  try {
    // Fetch initial tournament data on the server
    const tournament = await golfClient.tournaments.getTournament(id as string);

    return {
      props: {
        tournament,
        tournamentId: id as string,
      },
    };
  } catch (error) {
    console.error('Failed to fetch tournament:', error);
    
    return {
      notFound: true,
    };
  }
};
```

```javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  env: {
    NEXT_PUBLIC_SQUAD_GOLF_API_KEY: process.env.NEXT_PUBLIC_SQUAD_GOLF_API_KEY,
  },
  // Enable WebSocket support
  webpack: (config) => {
    config.resolve.fallback = {
      ...config.resolve.fallback,
      net: false,
      tls: false,
    };
    return config;
  },
};

module.exports = nextConfig;
```

```bash
# .env.local
NEXT_PUBLIC_SQUAD_GOLF_API_KEY=your_api_key_here
```

```json
// package.json dependencies to add
{
  "dependencies": {
    "@squadgolf/api-client": "^1.0.4",
    "date-fns": "^2.29.3"
  }
}
```

This Next.js example demonstrates:

- **🔧 Proper client setup** with environment variables
- **🪝 Custom React hook** for WebSocket leaderboard management  
- **🎨 Beautiful UI components** with Tailwind CSS styling
- **📱 Responsive design** that works on mobile and desktop
- **🔄 Real-time updates** with visual live indicators
- **⚠️ Error handling** for tier restrictions and connection issues
- **🚀 Server-side rendering** for initial data loading
- **💾 State management** with automatic reconnection
- **🎯 TypeScript support** throughout the entire application

The component automatically handles WebSocket connections, displays live updates with visual indicators, and gracefully handles errors like tier restrictions or connection failures.

## Development

```bash
# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Build the package
npm run build

# Run linting
npm run lint

# Format code
npm run format

# Type checking
npm run type-check
```

## Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history and breaking changes.

## Support & Resources

- 📚 **[API Documentation](https://docs.squad.golf)** - Complete API reference
- 💬 **[Discord Community](https://discord.gg/squadgolf)** - Join fellow developers
- 📧 **[Email Support](mailto:support@squad.golf)** - Direct support line
- 🐛 **[Bug Reports](https://github.com/Squad-Golf/api/issues)** - Report issues
- 💰 **[Pricing & Tiers](https://squad.golf/pricing)** - Upgrade your subscription

## License

MIT License - see [LICENSE](LICENSE) file for details.

---

Made with ⛳ by the [Squad Golf](https://squad.golf) team