Frontend
πŸ“˜ Frontend Complete Guide

Frontend Complete Guide

Comprehensive Frontend Development: Everything you need to build, test, and deploy the AltSportsLeagues.ai frontend application.

Frontend Architecture

Technology Stack

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Frontend Architecture                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                               β”‚
β”‚  Framework Layer                                             β”‚
β”‚  β”œβ”€ Next.js 16 (App Router)                                 β”‚
β”‚  β”œβ”€ React 18.2                                              β”‚
β”‚  └─ TypeScript 5.x                                          β”‚
β”‚                                                               β”‚
β”‚  UI Layer                                                    β”‚
β”‚  β”œβ”€ Tailwind CSS                                            β”‚
β”‚  β”œβ”€ shadcn/ui Components                                    β”‚
β”‚  β”œβ”€ Framer Motion (animations)                              β”‚
β”‚  └─ D3.js (graph visualizations)                            β”‚
β”‚                                                               β”‚
β”‚  State Management                                            β”‚
β”‚  β”œβ”€ Zustand (global state)                                  β”‚
β”‚  β”œβ”€ React Query (server state)                              β”‚
β”‚  └─ React Context (theme, auth)                             β”‚
β”‚                                                               β”‚
β”‚  Data Layer                                                  β”‚
β”‚  β”œβ”€ API Client (/lib/api-client.ts)                        β”‚
β”‚  β”œβ”€ Firebase (auth, real-time)                              β”‚
β”‚  └─ WebSocket (live updates)                                β”‚
β”‚                                                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Project Structure

clients/frontend/
β”œβ”€ app/                    # Next.js App Router
β”‚  β”œβ”€ layout.tsx           # Root layout
β”‚  β”œβ”€ page.tsx             # Homepage
β”‚  β”œβ”€ leagues/             # League pages
β”‚  β”‚  β”œβ”€ page.tsx          # List view
β”‚  β”‚  └─ [id]/page.tsx     # Detail view
β”‚  β”œβ”€ dashboard/           # User dashboard
β”‚  β”œβ”€ onboarding/          # Onboarding flow
β”‚  └─ api/                 # API routes (if needed)
β”‚
β”œβ”€ components/             # React components
β”‚  β”œβ”€ ui/                  # shadcn/ui base components
β”‚  β”œβ”€ leagues/             # League-specific components
β”‚  β”œβ”€ dashboard/           # Dashboard components
β”‚  └─ shared/              # Shared components
β”‚
β”œβ”€ lib/                    # Utilities and helpers
β”‚  β”œβ”€ api-client.ts        # API client
β”‚  β”œβ”€ firebase.ts          # Firebase config
β”‚  β”œβ”€ utils.ts             # Helper functions
β”‚  └─ constants.ts         # App constants
β”‚
β”œβ”€ hooks/                  # Custom React hooks
β”‚  β”œβ”€ useAuth.ts           # Authentication
β”‚  β”œβ”€ useLeagues.ts        # League data
β”‚  └─ useWebSocket.ts      # WebSocket connection
β”‚
β”œβ”€ store/                  # Zustand stores
β”‚  β”œβ”€ auth.ts              # Auth state
β”‚  β”œβ”€ leagues.ts           # League state
β”‚  └─ ui.ts                # UI state (sidebar, theme)
β”‚
β”œβ”€ types/                  # TypeScript types
β”‚  β”œβ”€ league.ts            # League types
β”‚  β”œβ”€ api.ts               # API response types
β”‚  └─ index.ts             # Barrel exports
β”‚
β”œβ”€ styles/                 # Global styles
β”‚  └─ globals.css          # Tailwind + custom CSS
β”‚
β”œβ”€ public/                 # Static assets
β”‚  β”œβ”€ images/              # Images
β”‚  └─ icons/               # Icons/logos
β”‚
└─ e2e/                    # Playwright tests
   β”œβ”€ leagues.spec.ts      # League tests
   └─ auth.spec.ts         # Auth tests

Getting Started

Initial Setup

# Clone and navigate
cd clients/frontend
 
# Install dependencies
npm install
 
# Set up environment variables
cp .env.example .env.local
 
# Required environment variables
NEXT_PUBLIC_API_URL=https://api.altsportsleagues.ai
NEXT_PUBLIC_FIREBASE_API_KEY=your_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id

Development Server

# Start dev server (port 3031)
npm run dev
 
# Start with production build locally
npm run build
npm start
 
# Open in browser
open http://localhost:3031

Development Workflow

  1. Create a feature branch

    git checkout -b feature/league-search
  2. Make changes (components, pages, styles)

  3. Test locally

    npm run dev
    # Test in browser
  4. Run type check

    npm run type-check
  5. Run linter

    npm run lint
  6. Run E2E tests

    npm run test:e2e
  7. Commit and push

    git add .
    git commit -m "feat: add league search functionality"
    git push origin feature/league-search

Component Development

Creating a New Component

// components/leagues/LeagueCard.tsx
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import Link from 'next/link';
import type { League } from '@/types/league';
 
interface LeagueCardProps {
  league: League;
  onClick?: () => void;
}
 
export function LeagueCard({ league, onClick }: LeagueCardProps) {
  return (
    <Card className="hover:shadow-lg transition-shadow cursor-pointer" onClick={onClick}>
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle>{league.name}</CardTitle>
          <Badge variant="secondary">{league.sport}</Badge>
        </div>
      </CardHeader>
      <CardContent>
        <div className="space-y-2 text-sm text-muted-foreground">
          <p>Region: {league.region}</p>
          <p>Teams: {league.teams_count}</p>
          <p>Founded: {league.founded}</p>
        </div>
        <Link
          href={`/leagues/${league.id}`}
          className="mt-4 inline-block text-primary hover:underline"
        >
          View Details β†’
        </Link>
      </CardContent>
    </Card>
  );
}

shadcn/ui Component Usage

# Add a new shadcn component
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
 
# Components are added to components/ui/
// Using shadcn components
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
 
export function MyComponent() {
  return (
    <Dialog>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>League Details</DialogTitle>
        </DialogHeader>
        <Button onClick={handleSave}>Save</Button>
      </DialogContent>
    </Dialog>
  );
}

Component Best Practices

  1. Use TypeScript for props

    interface Props {
      title: string;
      onSubmit: (data: FormData) => Promise<void>;
      disabled?: boolean;
    }
  2. Export components from index files

    // components/leagues/index.ts
    export { LeagueCard } from './LeagueCard';
    export { LeagueList } from './LeagueList';
    export { LeagueDetail } from './LeagueDetail';
  3. Use Tailwind utility classes

    <div className="flex items-center gap-4 p-4 rounded-lg bg-card">
  4. Implement loading and error states

    if (isLoading) return <Skeleton />;
    if (error) return <ErrorMessage error={error} />;
    return <LeagueList leagues={data} />;

State Management

Zustand Store Pattern

// store/leagues.ts
import { create } from 'zustand';
import type { League } from '@/types/league';
 
interface LeaguesState {
  leagues: League[];
  selectedLeague: League | null;
  isLoading: boolean;
  error: string | null;
 
  // Actions
  setLeagues: (leagues: League[]) => void;
  setSelectedLeague: (league: League | null) => void;
  addLeague: (league: League) => void;
  updateLeague: (id: string, updates: Partial<League>) => void;
  deleteLeague: (id: string) => void;
}
 
export const useLeaguesStore = create<LeaguesState>((set) => ({
  leagues: [],
  selectedLeague: null,
  isLoading: false,
  error: null,
 
  setLeagues: (leagues) => set({ leagues }),
 
  setSelectedLeague: (league) => set({ selectedLeague: league }),
 
  addLeague: (league) =>
    set((state) => ({ leagues: [...state.leagues, league] })),
 
  updateLeague: (id, updates) =>
    set((state) => ({
      leagues: state.leagues.map((l) =>
        l.id === id ? { ...l, ...updates } : l
      )
    })),
 
  deleteLeague: (id) =>
    set((state) => ({
      leagues: state.leagues.filter((l) => l.id !== id)
    }))
}));

Using Zustand Store

// components/leagues/LeagueList.tsx
import { useLeaguesStore } from '@/store/leagues';
 
export function LeagueList() {
  const leagues = useLeaguesStore((state) => state.leagues);
  const setSelectedLeague = useLeaguesStore((state) => state.setSelectedLeague);
 
  return (
    <div className="grid grid-cols-3 gap-4">
      {leagues.map((league) => (
        <LeagueCard
          key={league.id}
          league={league}
          onClick={() => setSelectedLeague(league)}
        />
      ))}
    </div>
  );
}

React Query for Server State

// hooks/useLeagues.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
 
export function useLeagues(params?: LeagueFilters) {
  return useQuery({
    queryKey: ['leagues', params],
    queryFn: () => apiClient.getLeagues(params),
    staleTime: 5 * 60 * 1000 // 5 minutes
  });
}
 
export function useLeague(id: string) {
  return useQuery({
    queryKey: ['league', id],
    queryFn: () => apiClient.getLeague(id),
    enabled: !!id
  });
}
 
export function useCreateLeague() {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: (data: LeagueInput) => apiClient.createLeague(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['leagues'] });
    }
  });
}

API Integration

API Client Setup

// lib/api-client.ts
import { auth } from './firebase';
 
class APIClient {
  private baseUrl: string;
 
  constructor() {
    this.baseUrl = process.env.NEXT_PUBLIC_API_URL || 'https://api.altsportsleagues.ai';
  }
 
  private async getToken(): Promise<string> {
    const user = auth.currentUser;
    if (!user) throw new Error('Not authenticated');
    return user.getIdToken();
  }
 
  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const token = await this.getToken();
 
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });
 
    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new Error(error.detail || response.statusText);
    }
 
    return response.json();
  }
 
  // League endpoints
  async getLeagues(params?: LeagueFilters) {
    const query = new URLSearchParams(params as any).toString();
    return this.request<LeaguesResponse>(`/v1/leagues?${query}`);
  }
 
  async getLeague(id: string) {
    return this.request<League>(`/v1/leagues/${id}`);
  }
 
  async createLeague(data: LeagueInput) {
    return this.request<League>('/v1/leagues', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
 
  async updateLeague(id: string, data: Partial<LeagueInput>) {
    return this.request<League>(`/v1/leagues/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data)
    });
  }
 
  async deleteLeague(id: string) {
    return this.request<void>(`/v1/leagues/${id}`, {
      method: 'DELETE'
    });
  }
 
  // Search
  async searchLeagues(query: string) {
    return this.request<League[]>(`/v1/leagues/discovery?query=${query}`);
  }
}
 
export const apiClient = new APIClient();

Error Handling

// lib/error-handler.ts
import { toast } from '@/components/ui/use-toast';
 
export function handleAPIError(error: unknown) {
  if (error instanceof Error) {
    const message = error.message;
 
    if (message.includes('401') || message.includes('Unauthorized')) {
      toast({
        title: 'Authentication Error',
        description: 'Please sign in again',
        variant: 'destructive'
      });
      // Redirect to login
      window.location.href = '/login';
      return;
    }
 
    if (message.includes('429') || message.includes('Too Many Requests')) {
      toast({
        title: 'Rate Limited',
        description: 'Too many requests. Please try again later.',
        variant: 'destructive'
      });
      return;
    }
 
    toast({
      title: 'Error',
      description: message,
      variant: 'destructive'
    });
  } else {
    toast({
      title: 'Unknown Error',
      description: 'Something went wrong',
      variant: 'destructive'
    });
  }
}

Routing & Navigation

App Router Page Structure

// app/leagues/page.tsx (List page)
import { LeagueList } from '@/components/leagues/LeagueList';
import { Suspense } from 'react';
 
export default function LeaguesPage() {
  return (
    <div className="container py-8">
      <h1 className="text-4xl font-bold mb-8">Sports Leagues</h1>
      <Suspense fallback={<LeagueListSkeleton />}>
        <LeagueList />
      </Suspense>
    </div>
  );
}
// app/leagues/[id]/page.tsx (Detail page)
import { notFound } from 'next/navigation';
import { LeagueDetail } from '@/components/leagues/LeagueDetail';
 
interface Props {
  params: { id: string };
}
 
export default async function LeagueDetailPage({ params }: Props) {
  const league = await getLeague(params.id);
 
  if (!league) {
    notFound();
  }
 
  return <LeagueDetail league={league} />;
}

Navigation Component

// components/shared/Navbar.tsx
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
 
const links = [
  { href: '/', label: 'Home' },
  { href: '/leagues', label: 'Leagues' },
  { href: '/dashboard', label: 'Dashboard' }
];
 
export function Navbar() {
  const pathname = usePathname();
 
  return (
    <nav className="border-b">
      <div className="container flex items-center gap-6 h-16">
        <Link href="/" className="font-bold text-xl">
          AltSportsLeagues
        </Link>
        <div className="flex gap-4">
          {links.map((link) => (
            <Link
              key={link.href}
              href={link.href}
              className={cn(
                'text-sm font-medium transition-colors hover:text-primary',
                pathname === link.href
                  ? 'text-primary'
                  : 'text-muted-foreground'
              )}
            >
              {link.label}
            </Link>
          ))}
        </div>
      </div>
    </nav>
  );
}

Programmatic Navigation

'use client';
 
import { useRouter } from 'next/navigation';
 
export function MyComponent() {
  const router = useRouter();
 
  const handleNavigate = () => {
    router.push('/leagues');
    // router.replace('/leagues'); // No history entry
    // router.back(); // Go back
    // router.refresh(); // Reload current route
  };
 
  return <button onClick={handleNavigate}>View Leagues</button>;
}

Authentication

Firebase Auth Setup

// lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
 
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID
};
 
export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const googleProvider = new GoogleAuthProvider();

Auth Hook

// hooks/useAuth.ts
import { useEffect, useState } from 'react';
import { auth } from '@/lib/firebase';
import { onAuthStateChanged, User } from 'firebase/auth';
 
export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });
 
    return () => unsubscribe();
  }, []);
 
  return { user, loading };
}

Login Component

// components/auth/LoginButton.tsx
'use client';
 
import { signInWithPopup, signOut } from 'firebase/auth';
import { auth, googleProvider } from '@/lib/firebase';
import { Button } from '@/components/ui/button';
import { useAuth } from '@/hooks/useAuth';
 
export function LoginButton() {
  const { user, loading } = useAuth();
 
  const handleLogin = async () => {
    try {
      await signInWithPopup(auth, googleProvider);
    } catch (error) {
      console.error('Login failed:', error);
    }
  };
 
  const handleLogout = async () => {
    try {
      await signOut(auth);
    } catch (error) {
      console.error('Logout failed:', error);
    }
  };
 
  if (loading) return <Button disabled>Loading...</Button>;
 
  if (user) {
    return (
      <div className="flex items-center gap-4">
        <span className="text-sm">{user.email}</span>
        <Button onClick={handleLogout} variant="outline">
          Sign Out
        </Button>
      </div>
    );
  }
 
  return (
    <Button onClick={handleLogin}>
      Sign in with Google
    </Button>
  );
}

Protected Routes

// components/auth/ProtectedRoute.tsx
'use client';
 
import { useAuth } from '@/hooks/useAuth';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
 
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { user, loading } = useAuth();
  const router = useRouter();
 
  useEffect(() => {
    if (!loading && !user) {
      router.push('/login');
    }
  }, [user, loading, router]);
 
  if (loading) {
    return <div>Loading...</div>;
  }
 
  if (!user) {
    return null;
  }
 
  return <>{children}</>;
}

Styling

Tailwind Configuration

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ['class'],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}'
  ],
  theme: {
    container: {
      center: true,
      padding: '2rem',
      screens: {
        '2xl': '1400px'
      }
    },
    extend: {
      colors: {
        border: 'hsl(var(--border))',
        input: 'hsl(var(--input))',
        ring: 'hsl(var(--ring))',
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        primary: {
          DEFAULT: 'hsl(var(--primary))',
          foreground: 'hsl(var(--primary-foreground))'
        }
      }
    }
  },
  plugins: [require('tailwindcss-animate')]
};

Global Styles

/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
  }
 
  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;
  }
}
 
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

Dark Mode Toggle

// components/ui/ThemeToggle.tsx
'use client';
 
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
 
export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
 
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

Testing

E2E Testing with Playwright

# Install Playwright
npm install -D @playwright/test
 
# Install browsers
npx playwright install
 
# Run tests
npm run test:e2e
 
# Run tests in UI mode
npx playwright test --ui
 
# Run specific test
npx playwright test leagues.spec.ts

Example E2E Test

// e2e/leagues.spec.ts
import { test, expect } from '@playwright/test';
 
test.describe('League Discovery', () => {
  test('should display league list', async ({ page }) => {
    await page.goto('http://localhost:3031/leagues');
 
    // Wait for leagues to load
    await page.waitForSelector('[data-testid="league-card"]');
 
    // Check that leagues are displayed
    const leagues = await page.locator('[data-testid="league-card"]').count();
    expect(leagues).toBeGreaterThan(0);
  });
 
  test('should search for leagues', async ({ page }) => {
    await page.goto('http://localhost:3031/leagues');
 
    // Enter search query
    await page.fill('[data-testid="search-input"]', 'basketball');
    await page.keyboard.press('Enter');
 
    // Check results
    await page.waitForSelector('[data-testid="league-card"]');
    const firstLeague = await page.locator('[data-testid="league-card"]').first();
    await expect(firstLeague).toContainText('basketball');
  });
 
  test('should navigate to league detail', async ({ page }) => {
    await page.goto('http://localhost:3031/leagues');
 
    // Click first league
    await page.locator('[data-testid="league-card"]').first().click();
 
    // Check URL and content
    expect(page.url()).toContain('/leagues/');
    await expect(page.locator('h1')).toBeVisible();
  });
});

Component Testing

// __tests__/LeagueCard.test.tsx
import { render, screen } from '@testing-library/react';
import { LeagueCard } from '@/components/leagues/LeagueCard';
 
const mockLeague = {
  id: 'nba',
  name: 'National Basketball Association',
  sport: 'basketball',
  region: 'north_america',
  teams_count: 30,
  founded: 1946
};
 
describe('LeagueCard', () => {
  it('renders league information', () => {
    render(<LeagueCard league={mockLeague} />);
 
    expect(screen.getByText('National Basketball Association')).toBeInTheDocument();
    expect(screen.getByText('basketball')).toBeInTheDocument();
    expect(screen.getByText(/Teams: 30/)).toBeInTheDocument();
  });
 
  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<LeagueCard league={mockLeague} onClick={handleClick} />);
 
    screen.getByText('National Basketball Association').click();
    expect(handleClick).toHaveBeenCalled();
  });
});

Deployment

Local Build

# Build for production
npm run build
 
# Start production server
npm start
 
# Test production build locally
open http://localhost:3031

Vercel Deployment

# Install Vercel CLI
npm install -g vercel
 
# Login to Vercel
vercel login
 
# Deploy (production)
vercel --prod
 
# Deploy (preview)
vercel

Environment Variables in Vercel

  1. Go to Vercel Dashboard β†’ Project β†’ Settings β†’ Environment Variables
  2. Add all required variables:
    • NEXT_PUBLIC_API_URL
    • NEXT_PUBLIC_FIREBASE_API_KEY
    • NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
    • etc.

vercel.json Configuration

{
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm install",
  "framework": "nextjs",
  "rewrites": [
    {
      "source": "/api/v1/:path*",
      "destination": "https://api.altsportsleagues.ai/v1/:path*"
    }
  ],
  "redirects": [
    {
      "source": "/docs/:path*",
      "destination": "https://docs.altsportsleagues.ai/:path*"
    }
  ]
}

Performance Optimization

Image Optimization

import Image from 'next/image';
 
<Image
  src="/logo.png"
  alt="Logo"
  width={200}
  height={50}
  priority // For above-the-fold images
/>

Code Splitting

// Dynamic imports for large components
import dynamic from 'next/dynamic';
 
const GraphVisualization = dynamic(
  () => import('@/components/GraphVisualization'),
  {
    ssr: false,
    loading: () => <p>Loading...</p>
  }
);

Caching Strategy

// React Query caching
const { data } = useQuery({
  queryKey: ['leagues'],
  queryFn: fetchLeagues,
  staleTime: 5 * 60 * 1000, // 5 minutes
  gcTime: 10 * 60 * 1000 // 10 minutes (was cacheTime)
});

Best Practices

  1. Use TypeScript everywhere - Catch errors at compile time
  2. Component composition - Build small, reusable components
  3. Server Components by default - Use 'use client' only when needed
  4. Optimize images - Use Next.js Image component
  5. Code splitting - Dynamic imports for large components
  6. Error boundaries - Catch and handle errors gracefully
  7. Loading states - Always show loading indicators
  8. Accessibility - Use semantic HTML and ARIA labels
  9. SEO optimization - Use metadata API in Next.js 13+
  10. Performance monitoring - Use Web Vitals

Troubleshooting

Common Issues

1. Hydration Errors

// Solution: Use useEffect for client-only code
useEffect(() => {
  // Client-only code
}, []);

2. API Token Expired

// Solution: Refresh token automatically
auth.onIdTokenChanged(async (user) => {
  if (user) {
    const token = await user.getIdToken(true);
    // Update token
  }
});

3. Build Errors

# Clear Next.js cache
rm -rf .next
npm run build

4. Type Errors

# Check types
npm run type-check
 
# Update types from backend
npm run sync-types

Resources


Last Updated: November 2024 Version: 2.1.0

Platform

Documentation

Community

Support

partnership@altsportsdata.comdev@altsportsleagues.ai

2025 Β© AltSportsLeagues.ai. Powered by AI-driven sports business intelligence.

πŸ€– AI-Enhancedβ€’πŸ“Š Data-Drivenβ€’βš‘ Real-Time