@squadgolf/api-client
v1.0.10
Published
TypeScript client library for Squad Golf API with intelligent caching and WebSocket support
Downloads
26
Maintainers
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.
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-clientQuick 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
- Visit Squad Golf Dashboard
- Sign up or log in to your account
- Generate an API key from your dashboard
- 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 backoffClient 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-checkContributing
We welcome contributions! Please see our Contributing Guide for details.
Changelog
See CHANGELOG.md for version history and breaking changes.
Support & Resources
- 📚 API Documentation - Complete API reference
- 💬 Discord Community - Join fellow developers
- 📧 Email Support - Direct support line
- 🐛 Bug Reports - Report issues
- 💰 Pricing & Tiers - Upgrade your subscription
License
MIT License - see LICENSE file for details.
Made with ⛳ by the Squad Golf team
