npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@squadgolf/api-client

v1.0.10

Published

TypeScript client library for Squad Golf API with intelligent caching and WebSocket support

Downloads

26

Readme

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 License: 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

npm install @squadgolf/api-client

Quick Start

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
  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
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

// 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

// 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

// 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

// 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

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

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

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

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

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:

// 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:

// 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

// 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:

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

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

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

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:

// 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,
  }
});
// 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
  };
}
// 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>
  );
}
// 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,
    };
  }
};
// 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;
# .env.local
NEXT_PUBLIC_SQUAD_GOLF_API_KEY=your_api_key_here
// 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

# 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 for details.

Changelog

See CHANGELOG.md for version history and breaking changes.

Support & Resources

License

MIT License - see LICENSE file for details.


Made with ⛳ by the Squad Golf team