Frontend
Frontend Next.js Application

Frontend Next.js Application

Introduction

This specification defines requirements for the AltSportsLeagues.ai Frontend Next.js Application - a modern, production-ready web application built with Next.js 16, TypeScript, and shadcn/ui, providing an intuitive interface for sports league partnership management and business intelligence.

Key Principle: Build a fast, accessible, and delightful user experience that seamlessly integrates with the backend API while supporting both human users and AI agents. The frontend serves as the primary interface for league managers, analysts, and business users, delivering real-time insights, interactive dashboards, and seamless workflows.

The application is designed to handle complex sports data visualization, AI-powered recommendations, document management, and collaborative features while maintaining high performance and accessibility standards. It leverages modern web technologies to provide a responsive, mobile-first experience across all devices.

Glossary

  • Frontend_App: Next.js 16 application utilizing the App Router for optimal performance and SEO
  • UI_Components: shadcn/ui component library built on Radix UI primitives for accessibility and consistency
  • Data_Layer: React Query for efficient server state management, caching, and synchronization
  • Auth_System: NextAuth.js v5 with JWT and OAuth providers for secure user authentication
  • Real_Time: WebSocket integration via Socket.io for live updates and collaborative features
  • Analytics: Comprehensive user behavior tracking and performance monitoring with Vercel Analytics
  • AI_Integration: MCP client library for AI-assisted workflows and intelligent features
  • Design_System: Consistent design tokens, components, and patterns using Tailwind CSS
  • Mobile_First: Responsive design prioritizing mobile experience with touch-friendly interactions
  • Performance: Core Web Vitals optimization, code splitting, and lazy loading for fast interactions

This glossary provides the terminology foundation for understanding the frontend architecture and its integration points with the broader AltSportsLeagues.ai ecosystem.

System Architecture

High-Level Architecture

The frontend architecture follows Next.js best practices, leveraging server components for data fetching, client components for interactivity, and a robust state management layer for optimal performance.

This architecture diagram illustrates the layered approach, where server components handle initial data fetching, client components provide rich interactivity, and state management ensures smooth user experience across the application.

Request Flow

The request flow optimizes for performance, leveraging server-side rendering where possible and client-side hydration for interactive elements.

This sequence diagram demonstrates the hybrid rendering approach: server-side rendering for initial load speed, client-side React Query for interactive updates, and WebSocket for real-time synchronization, ensuring both performance and responsiveness.

Project Structure

The frontend follows Next.js conventions with clear separation of concerns and domain organization.

clients/frontend/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app/                          # App Router pages and layouts
β”‚   β”‚   β”œβ”€β”€ (auth)/                    # Authentication group routes
β”‚   β”‚   β”‚   β”œβ”€β”€ login/                  # Login page
β”‚   β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ signup/                 # Signup page
β”‚   β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”‚   └── layout.tsx
β”‚   β”‚   β”œβ”€β”€ (dashboard)/                # Main application group
β”‚   β”‚   β”‚   β”œβ”€β”€ leagues/                 # League management
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx             # Leagues list
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ [id]/                # Individual league
β”‚   β”‚   β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”‚   β”‚   └── layout.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ documents/               # Document processing
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx
β”‚   β”‚   β”‚   β”‚   └── [id]/                # Document detail
β”‚   β”‚   β”‚   β”‚       └── page.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ contracts/               # Contract generation
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ page.tsx
β”‚   β”‚   β”‚   β”‚   └── [id]/                # Contract editor
β”‚   β”‚   β”‚   β”‚       └── page.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ analytics/               # Analytics dashboard
β”‚   β”‚   β”‚   β”‚   └── page.tsx
β”‚   β”‚   β”‚   └── layout.tsx               # Dashboard layout
β”‚   β”‚   β”œβ”€β”€ api/                         # API routes (Backend For You)
β”‚   β”‚   β”‚   β”œβ”€β”€ auth/                    # Auth API handlers
β”‚   β”‚   β”‚   β”‚   └── route.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ leagues/                 # League API
β”‚   β”‚   β”‚   β”‚   └── route.ts
β”‚   β”‚   β”‚   └── webhooks/                # Webhook handlers
β”‚   β”‚   β”‚       └── route.ts
β”‚   β”‚   β”œβ”€β”€ globals.css                  # Global styles
β”‚   β”‚   β”œβ”€β”€ layout.tsx                   # Root layout
β”‚   β”‚   β”œβ”€β”€ loading.tsx                  # Global loading UI
β”‚   β”‚   β”œβ”€β”€ not-found.tsx                # 404 page
β”‚   β”‚   β”œβ”€β”€ page.tsx                     # Homepage
β”‚   β”‚   └── providers.tsx                 # Context providers
β”‚   β”œβ”€β”€ components/                      # Reusable React components
β”‚   β”‚   β”œβ”€β”€ ui/                          # shadcn/ui components
β”‚   β”‚   β”‚   β”œβ”€β”€ button.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ card.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ table.tsx
β”‚   β”‚   β”‚   └── ... (full shadcn/ui set)
β”‚   β”‚   β”œβ”€β”€ leagues/                      # League-specific components
β”‚   β”‚   β”‚   β”œβ”€β”€ league-card.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ league-filters.tsx
β”‚   β”‚   β”‚   └── league-list.tsx
β”‚   β”‚   β”œβ”€β”€ documents/                    # Document components
β”‚   β”‚   β”‚   β”œβ”€β”€ document-uploader.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ document-viewer.tsx
β”‚   β”‚   β”‚   └── processing-status.tsx
β”‚   β”‚   β”œβ”€β”€ contracts/                    # Contract components
β”‚   β”‚   β”‚   β”œβ”€β”€ contract-editor.tsx
β”‚   β”‚   β”‚   └── signature-canvas.tsx
β”‚   β”‚   β”œβ”€β”€ analytics/                    # Analytics components
β”‚   β”‚   β”‚   β”œβ”€β”€ metrics-card.tsx
β”‚   β”‚   β”‚   └── trend-chart.tsx
β”‚   β”‚   └── shared/                       # Shared utilities
β”‚   β”‚       β”œβ”€β”€ header.tsx
β”‚   β”‚       β”œβ”€β”€ sidebar.tsx
β”‚   β”‚       └── modal.tsx
β”‚   β”œβ”€β”€ lib/                              # Library functions and hooks
β”‚   β”‚   β”œβ”€β”€ api/                          # API clients and types
β”‚   β”‚   β”‚   β”œβ”€β”€ client.ts                 # Axios instance
β”‚   β”‚   β”‚   β”œβ”€β”€ leagues.ts                # League API functions
β”‚   β”‚   β”‚   └── types.ts                  # API response types
β”‚   β”‚   β”œβ”€β”€ auth/                         # Authentication utilities
β”‚   β”‚   β”‚   └── config.ts                 # NextAuth configuration
β”‚   β”‚   β”œβ”€β”€ hooks/                        # Custom React hooks
β”‚   β”‚   β”‚   β”œβ”€β”€ use-league-analysis.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ use-web-socket.ts
β”‚   β”‚   β”‚   └── use-local-storage.ts
β”‚   β”‚   β”œβ”€β”€ utils/                        # Utility functions
β”‚   β”‚   β”‚   β”œβ”€β”€ cn.ts                     # ClassName utility
β”‚   β”‚   β”‚   β”œβ”€β”€ formatters.ts             # Data formatting
β”‚   β”‚   β”‚   └── validators.ts             # Client-side validation
β”‚   β”‚   └── validations/                   # Zod schemas for forms
β”‚   β”‚       └── league.ts
β”‚   β”œβ”€β”€ styles/                           # Global styles and themes
β”‚   β”‚   └── globals.css
β”‚   └── types/                            # Global TypeScript types
β”‚       β”œβ”€β”€ api.ts                        # API types
β”‚       β”œβ”€β”€ components.ts                 # Component props
β”‚       └── schemas.ts                    # Backend schema types
β”œβ”€β”€ public/                               # Static assets
β”‚   β”œβ”€β”€ images/                           # Images and icons
β”‚   └── fonts/                            # Custom fonts
β”œβ”€β”€ tests/                                # Test files
β”‚   β”œβ”€β”€ unit/                             # Unit tests (Vitest)
β”‚   β”‚   └── components/
β”‚   β”œβ”€β”€ integration/                      # Integration tests
β”‚   └── e2e/                              # End-to-end tests (Playwright)
β”œβ”€β”€ .env.local                            # Local environment variables
β”œβ”€β”€ .env.example                          # Environment template
β”œβ”€β”€ next.config.js                        # Next.js configuration
β”œβ”€β”€ tailwind.config.js                    # Tailwind CSS configuration
β”œβ”€β”€ tsconfig.json                         # TypeScript configuration
β”œβ”€β”€ eslint.config.js                      # ESLint configuration
β”œβ”€β”€ prettier.config.js                    # Prettier configuration
β”œβ”€β”€ package.json                          # Dependencies and scripts
└── README.md                             # Project documentation

This structure provides clear separation between page routes, reusable components, API integrations, and supporting libraries, making the codebase maintainable and scalable.

Component Design

The frontend utilizes shadcn/ui for consistent, accessible components built on Radix UI primitives. Below are key component examples.

1. App Router Pages

File: src/app/(dashboard)/leagues/page.tsx

import { Suspense } from 'react';
import { LeagueList } from '@/components/leagues/league-list';
import { LeagueFilters } from '@/components/leagues/league-filters';
import { LeaguesSkeleton } from '@/components/leagues/leagues-skeleton';
import { Search, Filter, Plus } from 'lucide-react';
 
export const metadata = {
  title: 'Leagues | AltSportsLeagues',
  description: 'Manage and analyze sports league partnerships across global markets'
};
 
export default function LeaguesPage() {
  return (
    <div className="container mx-auto py-8 px-4">
      {/* Header Section */}
      <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8 gap-4">
        <div className="flex-1 min-w-0">
          <h1 className="text-3xl font-bold tracking-tight">
            League Partnership Pipeline
          </h1>
          <p className="text-muted-foreground mt-2">
            Discover, analyze, and manage sports league partnership opportunities
          </p>
        </div>
        
        {/* Action Buttons */}
        <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
          <LeagueFilters />
          <Button size="sm" className="gap-2">
            <Plus className="h-4 w-4" />
            Add League
          </Button>
        </div>
      </div>
 
      {/* Search and Filters */}
      <Card className="mb-6">
        <CardContent className="p-6">
          <div className="flex flex-col lg:flex-row gap-4">
            <div className="relative flex-1">
              <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
              <Input
                placeholder="Search leagues by name, sport, or location..."
                className="pl-10"
              />
            </div>
            <div className="flex gap-2">
              <Button variant="outline" size="sm">
                <Filter className="h-4 w-4 mr-2" />
                Filters
              </Button>
            </div>
          </div>
        </CardContent>
      </Card>
 
      {/* League List */}
      <Suspense key="leagues" fallback={<LeaguesSkeleton />}>
        <LeagueList />
      </Suspense>
    </div>
  );
}

This page demonstrates the integration of server-side rendering for initial data, React Query for dynamic updates, and shadcn/ui components for consistent styling.

2. React Query Integration

File: src/lib/api/leagues.ts

The data layer uses React Query for efficient server state management with built-in caching, refetching, and optimistic updates.

import { useQuery, useMutation, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { useSession } from 'next-auth/react';
import { apiClient } from './client';
import type { League, LeagueFilters, LeagueAnalysisRequest, LeagueAnalysisResult } from '@/types/league';
 
// Query keys for cache invalidation
export const leagueKeys = {
  all: ['leagues'] as const,
  lists: () => [...leagueKeys.all, 'list'] as const,
  list: (filters: Partial<LeagueFilters>) => [...leagueKeys.lists(), { filters: JSON.stringify(filters) }] as const,
  details: () => [...leagueKeys.all, 'detail'] as const,
  detail: (id: string) => [...leagueKeys.details(), id] as const,
  analysis: (leagueId: string) => [...leagueKeys.detail(leagueId), 'analysis'] as const,
};
 
// Fetch leagues with filters
export function useLeagues(filters: Partial<LeagueFilters> = {}) {
  const { data: session } = useSession();
  
  return useQuery({
    queryKey: leagueKeys.list(filters),
    queryFn: async () => {
      const response = await apiClient.get<League[]>('/leagues', {
        params: filters,
        headers: {
          Authorization: `Bearer ${session?.accessToken}`,
        },
      });
      return response.data;
    },
    staleTime: 5 * 60 * 1000, // 5 minutes stale time
    placeholderData: [], // Empty array as placeholder
    select: (data) => data, // Can transform data here
  });
}
 
// Fetch single league with suspense
export function useLeague(id: string) {
  return useSuspenseQuery({
    queryKey: leagueKeys.detail(id),
    queryFn: async () => {
      const response = await apiClient.get<League>(`/leagues/${id}`);
      return response.data;
    },
    enabled: !!id && id !== 'new', // Don't fetch for new league creation
  });
}
 
// Analyze league mutation with optimistic updates
export function useAnalyzeLeague() {
  const queryClient = useQueryClient();
  const { data: session } = useSession();
 
  return useMutation({
    mutationFn: async (request: LeagueAnalysisRequest) => {
      const response = await apiClient.post<LeagueAnalysisResult>(
        `/leagues/${request.league_id}/analysis`,
        request,
        {
          headers: {
            Authorization: `Bearer ${session?.accessToken}`,
          },
        }
      );
      return response.data;
    },
    onMutate: async (newAnalysis: LeagueAnalysisRequest) {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: leagueKeys.detail(newAnalysis.league_id) });
 
      // Snapshot previous value
      const previousLeague = queryClient.getQueryData<League>(leagueKeys.detail(newAnalysis.league_id));
 
      // Optimistically update to loading state
      queryClient.setQueryData(leagueKeys.detail(newAnalysis.league_id), (old: League | undefined) => ({
        ...old!,
        isAnalyzing: true,
        analysisStatus: 'pending',
      }));
 
      // Return context with undo function
      return { previousLeague };
    },
    onError: (err, newAnalysis, context) => {
      // Rollback on error
      if (context?.previousLeague) {
        queryClient.setQueryData(leagueKeys.detail(newAnalysis.league_id), context.previousLeague);
      }
    },
    onSuccess: (result, newAnalysis) => {
      // Update the league with analysis results
      queryClient.setQueryData(leagueKeys.detail(newAnalysis.league_id), (old: League | undefined) => ({
        ...old!,
        analysis: result,
        lastAnalyzed: new Date().toISOString(),
        analysisStatus: 'completed',
        isAnalyzing: false,
      }));
 
      // Invalidate and refetch other queries
      queryClient.invalidateQueries({ queryKey: leagueKeys.lists() });
    },
    onSettled: (data, error, variables) => {
      // Always remove loading state
      queryClient.setQueryData(leagueKeys.detail(variables.league_id), (old: League | undefined) => ({
        ...old!,
        isAnalyzing: false,
        analysisStatus: data ? 'completed' : 'failed',
      }));
    },
  });
}
 
// Infinite query for paginated leagues
export function useInfiniteLeagues(filters: Partial<LeagueFilters> = {}) {
  return useInfiniteQuery({
    queryKey: leagueKeys.list(filters),
    queryFn: async ({ pageParam = 1 }) => {
      const response = await apiClient.get<League[]>('/leagues', {
        params: { ...filters, page: pageParam, limit: 20 },
      });
      return response.data;
    },
    getNextPageParam: (lastPage, pages) => {
      return lastPage.length === 20 ? pages.length + 1 : undefined;
    },
    initialPageParam: 1,
  });
}

This React Query integration provides automatic caching, background refetching, optimistic updates, and seamless error handling, ensuring a smooth user experience.

3. Client Component Example

File: src/components/leagues/league-card.tsx

'use client';
 
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useAnalyzeLeague } from '@/lib/api/leagues';
import { League } from '@/types/league';
import { 
  TrendingUp, 
  Calendar, 
  MapPin, 
  Users, 
  DollarSign, 
  Loader2, 
  AlertCircle 
} from 'lucide-react';
import { cn } from '@/lib/utils';
 
interface LeagueCardProps {
  league: League;
  onSelect: (league: League) => void;
}
 
export function LeagueCard({ league, onSelect }: LeagueCardProps) {
  const [isExpanded, setIsExpanded] = useState(false);
  const analyzeLeague = useAnalyzeLeague();
 
  const handleAnalyze = () => {
    analyzeLeague.mutate({
      league_id: league.id,
      analysis_type: 'partnership',
      include_financials: true,
      include_market_data: true,
    });
  };
 
  const getTierVariant = (tier: string) => {
    if (tier.startsWith('tier_1')) return 'default' as const;
    if (tier.startsWith('tier_2')) return 'secondary' as const;
    if (tier.startsWith('tier_3')) return 'outline' as const;
    return 'ghost' as const;
  };
 
  const getTierColor = (tier: string) => {
    switch (getTierVariant(tier)) {
      case 'default': return 'bg-primary text-primary-foreground';
      case 'secondary': return 'bg-secondary text-secondary-foreground';
      case 'outline': return 'bg-muted text-muted-foreground';
      default: return 'bg-accent text-accent-foreground';
    }
  };
 
  return (
    <Card 
      className={cn(
        "w-full group cursor-pointer transition-all duration-200 hover:shadow-lg hover:-translate-y-1",
        "border-border hover:border-primary/50",
        league.isAnalyzing && "animate-pulse"
      )}
      onClick={() => onSelect(league)}
    >
      <CardHeader className="pb-3">
        <div className="flex items-start justify-between">
          <div className="space-y-1">
            <CardTitle className="text-lg font-semibold leading-tight">
              {league.name}
            </CardTitle>
            <CardDescription className="text-sm text-muted-foreground">
              {league.sport} β€’ {league.level}
            </CardDescription>
          </div>
          <Badge 
            variant={getTierVariant(league.tier)} 
            className={getTierColor(league.tier)}
          >
            {league.tier.replace('_', ' ').toUpperCase()}
          </Badge>
        </div>
      </CardHeader>
 
      <CardContent className="pb-4">
        <div className="space-y-3 text-sm">
          <div className="flex items-center gap-2">
            <MapPin className="h-4 w-4 text-muted-foreground" />
            <span>{league.primary_location}</span>
          </div>
          
          {league.member_count && (
            <div className="flex items-center gap-2">
              <Users className="h-4 w-4 text-muted-foreground" />
              <span>{league.member_count.toLocaleString()} members</span>
            </div>
          )}
 
          {league.founded_year && (
            <div className="flex items-center gap-2">
              <Calendar className="h-4 w-4 text-muted-foreground" />
              <span>Founded {league.founded_year}</span>
            </div>
          )}
 
          {league.analysis && (
            <div className="space-y-1">
              <div className="flex items-center gap-2">
                <TrendingUp className={cn(
                  "h-4 w-4",
                  league.analysis.opportunity_score > 70 ? "text-green-600" : "text-amber-600"
                )} />
                <span className="font-semibold">
                  Score: {league.analysis.opportunity_score}/100
                </span>
              </div>
              {league.analysis.summary && (
                <p className="text-xs text-muted-foreground mt-1">
                  {league.analysis.summary.substring(0, 100)}...
                </p>
              )}
            </div>
          )}
        </div>
      </CardContent>
 
      <CardFooter className="pt-4 border-t">
        <div className="flex items-center justify-between w-full">
          <Button
            variant="ghost"
            size="sm"
            onClick={(e) => {
              e.stopPropagation();
              setIsExpanded(!isExpanded);
            }}
            className="h-8 px-2 text-xs"
          >
            {isExpanded ? 'Show Less' : 'Show More'}
          </Button>
          
          <Button
            size="sm"
            onClick={(e) => {
              e.stopPropagation();
              handleAnalyze();
            }}
            disabled={analyzeLeague.isPending}
            className="h-8 px-3"
          >
            {analyzeLeague.isPending ? (
              <>
                <Loader2 className="h-4 w-4 mr-2 animate-spin" />
                Analyzing...
              </>
            ) : league.analysis ? (
              <>
                <TrendingUp className="h-4 w-4 mr-2" />
                Re-analyze
              </>
            ) : (
              <>
                <TrendingUp className="h-4 w-4 mr-2" />
                Analyze
              </>
            )}
          </Button>
        </div>
        
        {isExpanded && league.analysis && (
          <div className="mt-3 pt-3 border-t text-xs text-muted-foreground">
            <p><strong>Recommendations:</strong></p>
            <ul className="list-disc list-inside space-y-1 mt-1">
              {league.analysis.recommendations.slice(0, 3).map((rec, index) => (
                <li key={index}>{rec}</li>
              ))}
              {league.analysis.recommendations.length > 3 && (
                <li>... and {league.analysis.recommendations.length - 3} more</li>
              )}
            </ul>
          </div>
        )}
      </CardFooter>
    </Card>
  );
}

This component showcases integration of shadcn/ui, React state management, and API mutations with optimistic updates, providing a rich, interactive user experience.

4. Authentication with NextAuth.js

File: src/lib/auth/config.ts

NextAuth.js provides secure, flexible authentication supporting multiple providers.

import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import { apiClient } from '@/lib/api/client';
import { User } from '@/types/user';
 
// Extend the built-in session types
declare module 'next-auth' {
  interface Session {
    user: {
      id: string;
      email: string;
      name: string;
      role: string;
      accessToken: string;
    };
  }
 
  interface User {
    id: string;
    email: string;
    name: string;
    role: string;
    accessToken: string;
  }
}
 
export const authOptions: NextAuthOptions = {
  providers: [
    // Email & Password Provider
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Missing credentials');
        }
 
        try {
          const response = await apiClient.post<{
            user: User;
            access_token: string;
          }>('/auth/login', {
            email: credentials.email,
            password: credentials.password,
          });
 
          if (response.data) {
            return {
              id: response.data.user.id,
              email: response.data.user.email,
              name: response.data.user.name,
              role: response.data.user.role,
              accessToken: response.data.access_token,
            };
          }
          return null;
        } catch (error) {
          console.error('Authentication error:', error);
          return null;
        }
      },
    }),
 
    // Google OAuth Provider
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      authorization: {
        params: {
          scope: 'openid email profile',
          access_type: 'offline',
          prompt: 'consent',
        },
      },
    }),
  ],
 
  // Database adapter for production (optional)
  // adapter: PrismaAdapter(prisma),
 
  callbacks: {
    async jwt({ token, user, account }) {
      // Initial sign in
      if (user) {
        token.accessToken = user.accessToken as string;
        token.role = user.role as string;
      }
 
      // Return previous token if already signed in
      return token;
    },
 
    async session({ session, token }) {
      if (token) {
        session.user.id = token.sub as string;
        session.user.accessToken = token.accessToken as string;
        session.user.role = token.role as string;
      }
      return session;
    },
  },
 
  pages: {
    signIn: '/login',
    signOut: '/logout',
    error: '/auth/error',
  },
 
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
 
  // Security options
  secret: process.env.NEXTAUTH_SECRET!,
  debug: process.env.NODE_ENV === 'development',
};

File: src/app/api/auth/[...nextauth]/route.ts

import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth/config';
 
const handler = NextAuth(authOptions);
 
export { handler as GET, handler as POST };

5. API Client with Interceptors

File: src/lib/api/client.ts

A centralized API client handles authentication, error handling, and response formatting.

import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
import { getSession } from 'next-auth/react';
import { toast } from 'sonner';
 
class ApiClient {
  private client: AxiosInstance;
 
  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 30_000, // 30 seconds
      headers: {
        'Content-Type': 'application/json',
      },
    });
 
    // Request interceptor for authentication
    this.client.interceptors.request.use(
      async (config) => {
        const session = await getSession();
        if (session?.user?.accessToken) {
          config.headers.Authorization = `Bearer ${session.user.accessToken}`;
        }
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
 
    // Response interceptor for error handling
    this.client.interceptors.response.use(
      (response) => {
        // Handle successful responses
        if (response.status >= 200 && response.status < 300) {
          return response.data;
        }
        return Promise.reject(new Error(`HTTP error! status: ${response.status}`));
      },
      async (error: AxiosError) => {
        const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
 
        // Handle 401 Unauthorized (token expired)
        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          
          try {
            const session = await getSession();
            if (session?.user?.accessToken) {
              originalRequest.headers.Authorization = `Bearer ${session.user.accessToken}`;
              return this.client(originalRequest);
            }
          } catch (refreshError) {
            // Refresh failed, redirect to login
            if (typeof window !== 'undefined') {
              window.location.href = '/login?error=session_expired';
            }
            return Promise.reject(refreshError);
          }
        }
 
        // Handle 429 Too Many Requests
        if (error.response?.status === 429) {
          toast.error('Rate limit exceeded. Please try again in a moment.');
          return Promise.reject(error);
        }
 
        // Handle 403 Forbidden
        if (error.response?.status === 403) {
          toast.error('Access denied. Insufficient permissions.');
          return Promise.reject(error);
        }
 
        // Handle other errors
        if (error.response?.status >= 400 && error.response?.status < 500) {
          toast.error(error.response.data?.message || 'Request failed');
        } else if (error.response?.status >= 500) {
          toast.error('Server error. Please try again later.');
        }
 
        return Promise.reject(error);
      }
    );
  }
 
  get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get<T>(url, config);
  }
 
  post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config);
  }
 
  put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put<T>(url, data, config);
  }
 
  delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete<T>(url, config);
  }
}
 
export const apiClient = new ApiClient(
  process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'
);

State Management Strategy

React Query for Server State

React Query manages server state, caching, and synchronization across the application.

// src/app/providers.tsx
'use client';
 
import { 
  QueryClient, 
  QueryClientProvider, 
  useQueryErrorResetBoundary 
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ErrorBoundary } from 'react-error-boundary';
import { useState } from 'react';
 
export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 5 * 60 * 1000, // 5 minutes
            cacheTime: 10 * 60 * 1000, // 10 minutes
            refetchOnWindowFocus: false, // Don't refetch on focus
            retry: (failureCount, error) => {
              // Don't retry 4xx errors
              if (error.response?.status >= 400 && error.response?.status < 500) {
                return false;
              }
              return failureCount < 3; // Retry up to 3 times
            },
            retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
          },
          mutations: {
            retry: false, // Don't retry mutations
          },
        },
      })
  );
 
  return (
    <QueryClientProvider client={queryClient}>
      <ErrorBoundary
        fallbackRender={({ error, resetErrorBoundary }) => (
          <div className="container mx-auto px-4 py-8">
            <Card>
              <CardContent className="pt-6">
                <div className="space-y-4">
                  <h2 className="text-2xl font-bold">Something went wrong</h2>
                  <p className="text-muted-foreground">
                    An unexpected error occurred. Please try again.
                  </p>
                  <div className="flex gap-2">
                    <Button onClick={() => resetErrorBoundary()}>
                      Try Again
                    </Button>
                    <Button variant="outline" onClick={() => window.location.reload()}>
                      Reload Page
                    </Button>
                  </div>
                  <details className="mt-4 text-sm text-muted-foreground">
                    <summary>Details</summary>
                    <pre className="mt-2 p-2 bg-muted rounded text-xs overflow-auto">
                      {error.message}
                    </pre>
                  </details>
                </div>
              </CardContent>
            </Card>
          </div>
        )}
        onReset={() => {
          queryClient.reset();
        }}
      >
        {children}
        {process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
      </ErrorBoundary>
    </QueryClientProvider>
  );
}

Zustand for Client State

Zustand manages lightweight client state like UI preferences and temporary selections.

// src/lib/store/ui-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { StateCreator } from 'zustand';
 
interface UIState {
  // Theme management
  theme: 'light' | 'dark' | 'system';
  setTheme: (theme: 'light' | 'dark' | 'system') => void;
  
  // Sidebar and layout
  sidebarCollapsed: boolean;
  toggleSidebar: () => void;
  
  // Recently viewed items
  recentlyViewed: string[];
  addRecentlyViewed: (id: string, type: 'league' | 'document' | 'contract') => void;
  
  // Temporary selections
  selectedLeagueId: string | null;
  setSelectedLeague: (id: string | null) => void;
  
  // Notification preferences
  notificationsEnabled: boolean;
  setNotificationsEnabled: (enabled: boolean) => void;
}
 
const createUIStore: StateCreator<UIState> = (set, get) => ({
  theme: 'system',
  sidebarCollapsed: false,
  recentlyViewed: [],
  selectedLeagueId: null,
 
  setTheme: (theme) => set({ theme }),
  
  toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
  
  addRecentlyViewed: (id, type) =>
    set((state) => {
      const key = `${type}-${id}`;
      const viewed = state.recentlyViewed.filter((item) => item !== key);
      return {
        recentlyViewed: [key, ...viewed].slice(0, 10), // Keep last 10
      };
    }),
 
  setSelectedLeague: (id) => set({ selectedLeagueId: id }),
 
  notificationsEnabled: true,
  setNotificationsEnabled: (enabled) => set({ notificationsEnabled: enabled }),
});
 
export const useUIStore = create<UIState>()(
  persist(createUIStore, {
    name: 'altsports-ui-storage',
    storage: createJSONStorage(() => localStorage),
    partialize: (state) => ({
      theme: state.theme,
      sidebarCollapsed: state.sidebarCollapsed,
      notificationsEnabled: state.notificationsEnabled,
    }), // Don't persist temporary selections
  })
);

Performance Optimization

The frontend is optimized for Core Web Vitals and production performance.

1. Image Optimization with Next.js Image

Next.js Image component provides automatic optimization, lazy loading, and responsive sizing.

// Optimized image usage
import Image from 'next/image';
 
function LeagueLogo({ src, alt, priority = false }: { src: string; alt: string; priority?: boolean }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={120}
      height={60}
      priority={priority} // Only for above-the-fold images
      placeholder="blur" // Show blur while loading
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAA==" // Small blur
      className="rounded-lg object-cover"
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      onError={(e) => {
        // Fallback to placeholder
        e.currentTarget.src = '/placeholder-league-logo.svg';
      }}
    />
  );
}

2. Code Splitting and Dynamic Imports

Heavy components and libraries are loaded dynamically to reduce initial bundle size.

// Dynamic import for heavy chart component
import dynamic from 'next/dynamic';
 
const AdvancedLeagueChart = dynamic(() => import('@/components/charts/advanced-league-chart'), {
  loading: () => (
    <div className="flex items-center justify-center p-8">
      <Loader2 className="h-8 w-8 animate-spin text-primary mr-2" />
      Loading analytics...
    </div>
  ),
  ssr: false, // Render only on client
});
 
// Usage in component
function LeagueAnalytics({ leagueId }: { leagueId: string }) {
  return (
    <div className="mt-6">
      <AdvancedLeagueChart leagueId={leagueId} />
    </div>
  );
}

3. React Server Components for Data Fetching

Server components handle initial data loading, reducing client bundle size and improving LCP.

// app/leagues/[id]/page.tsx - Server Component
import { notFound } from 'next/navigation';
import { apiClient } from '@/lib/api/client';
import { LeagueCard } from '@/components/leagues/league-card';
import { LeagueActions } from '@/components/leagues/league-actions';
 
interface LeaguePageProps {
  params: { id: string };
}
 
async function getLeague(id: string) {
  try {
    const response = await apiClient.get(`/leagues/${id}`);
    return response;
  } catch (error) {
    console.error('Failed to fetch league:', error);
    return null;
  }
}
 
export default async function LeaguePage({ params }: LeaguePageProps) {
  const leagueData = await getLeague(params.id);
 
  if (!leagueData) {
    notFound();
  }
 
  const league = leagueData;
 
  return (
    <div className="container mx-auto py-8 px-4">
      <div className="mb-8">
        <LeagueCard league={league} onSelect={() => {}} />
      </div>
      
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <div className="lg:col-span-2">
          {/* Main content */}
          <LeagueActions league={league} />
        </div>
        
        <div className="space-y-6">
          {/* Sidebar content */}
          <RecentActivity leagueId={league.id} />
        </div>
      </div>
    </div>
  );
}

Testing Strategy

Comprehensive testing ensures reliability across unit, integration, and end-to-end scenarios.

1. Unit Tests with Vitest

Vitest provides fast, Jest-compatible unit testing for components and utilities.

File: tests/unit/components/league-card.test.tsx

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { LeagueCard } from '@/components/leagues/league-card';
import { useAnalyzeLeague } from '@/lib/api/leagues';
 
vi.mock('@/lib/api/leagues', () => ({
  useAnalyzeLeague: vi.fn(),
}));
 
const mockLeague = {
  id: '1',
  name: 'Test League',
  sport: 'Football',
  tier: 'tier_2',
  primary_location: 'New York, NY',
  member_count: 12,
  founded_year: 2015,
  analysis: {
    opportunity_score: 75,
    summary: 'Promising league with strong growth potential',
    recommendations: ['Target technology partners', 'Enhance digital presence'],
  },
};
 
vi.mocked(useAnalyzeLeague).mockReturnValue({
  mutate: vi.fn(),
  isPending: false,
} as any);
 
describe('LeagueCard', () => {
  let queryClient: QueryClient;
 
  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
      },
    });
  });
 
  it('renders league information correctly', () => {
    render(
      <QueryClientProvider client={queryClient}>
        <LeagueCard league={mockLeague} onSelect={() => {}} />
      </QueryClientProvider>
    );
 
    expect(screen.getByText('Test League')).toBeInTheDocument();
    expect(screen.getByText('Football')).toBeInTheDocument();
    expect(screen.getByText('tier_2')).toBeInTheDocument();
    expect(screen.getByText('New York, NY')).toBeInTheDocument();
  });
 
  it('expands to show more details', async () => {
    render(
      <QueryClientProvider client={queryClient}>
        <LeagueCard league={mockLeague} onSelect={() => {}} />
      </QueryClientProvider>
    );
 
    const showMoreButton = screen.getByRole('button', { name: /Show More/i });
    fireEvent.click(showMoreButton);
 
    await waitFor(() => {
      expect(screen.getByText('12 members')).toBeInTheDocument();
      expect(screen.getByText('Founded 2015')).toBeInTheDocument();
      expect(screen.getByText('Score: 75/100')).toBeInTheDocument();
    });
  });
 
  it('shows analyzing state during mutation', () => {
    vi.mocked(useAnalyzeLeague).mockReturnValue({
      mutate: vi.fn(),
      isPending: true,
    } as any);
 
    render(
      <QueryClientProvider client={queryClient}>
        <LeagueCard league={mockLeague} onSelect={() => {}} />
      </QueryClientProvider>
    );
 
    const analyzeButton = screen.getByRole('button', { name: /Analyzing.../i });
    expect(analyzeButton).toBeInTheDocument();
    expect(analyzeButton).toBeDisabled();
  });
 
  it('handles analyze click correctly', () => {
    const mockMutate = vi.fn();
    vi.mocked(useAnalyzeLeague).mockReturnValue({
      mutate: mockMutate,
      isPending: false,
    } as any);
 
    render(
      <QueryClientProvider client={queryClient}>
        <LeagueCard league={mockLeague} onSelect={() => {}} />
      </QueryClientProvider>
    );
 
    const analyzeButton = screen.getByRole('button', { name: /Analyze/i });
    fireEvent.click(analyzeButton);
 
    expect(mockMutate).toHaveBeenCalledWith({
      league_id: '1',
      analysis_type: 'partnership',
      include_financials: true,
      include_market_data: true,
    });
  });
});

2. End-to-End Tests with Playwright

Playwright ensures end-to-end workflow validation across the entire application.

File: tests/e2e/league-workflow.spec.ts

import { test, expect } from '@playwright/test';
import { faker } from '@faker-js/faker';
 
test.describe('League Management Workflow', () => {
  test('complete league analysis workflow', async ({ page }) => {
    // Arrange: Setup test data
    const testEmail = faker.internet.email();
    const testPassword = faker.internet.password();
 
    // Act: Login as test user
    await page.goto('/login');
    await page.fill('input[name=email]', testEmail);
    await page.fill('input[name=password]', testPassword);
    await page.click('button[type=submit]');
    
    // Assert: Redirected to dashboard
    await expect(page).toHaveURL(/dashboard/);
 
    // Act: Navigate to leagues page
    await page.click('text=Leagues');
    await expect(page).toHaveURL(/leagues/);
 
    // Act: Add new league
    await page.click('text=Add League');
    await expect(page).toHaveURL(/leagues\/new/);
 
    // Act: Fill league form
    await page.fill('input[name=league_name]', 'Test Integration League');
    await page.fill('input[name=website_url]', 'https://testleague.com');
    await page.fill('input[name=contact_email]', 'contact@testleague.com');
    await page.selectOption('select[name=sport_bucket]', 'team');
    await page.click('button:has-text("Create League")');
 
    // Assert: League created successfully
    await expect(page.locator('text=Test Integration League')).toBeVisible();
    await expect(page).toHaveURL(/leagues\/[a-f0-9-]+/);
 
    // Act: Analyze the league
    await page.click('button:has-text("Analyze")');
    
    // Assert: Analysis in progress
    await expect(page.locator('text=Analyzing...')).toBeVisible({ timeout: 15000 });
    
    // Act: Wait for analysis completion
    await page.waitForSelector('text=Score:', { timeout: 30000 });
 
    // Assert: Analysis results displayed
    await expect(page.locator('text=Score:')).toBeVisible();
    await expect(page.locator('text=Test Integration League')).toBeVisible();
  });
 
  test('search and filter leagues', async ({ page }) => {
    // Act: Navigate to leagues
    await page.goto('/leagues');
    
    // Assert: Leagues page loaded
    await expect(page.locator('h1')).toContainText('Leagues');
 
    // Act: Search for a league
    await page.fill('input[placeholder*="Search leagues"]', 'Premier');
    
    // Assert: Search results filtered
    await expect(page.locator('text=Premier')).toBeVisible({ timeout: 5000 });
    
    // Act: Apply filter (e.g., sport = soccer)
    await page.click('button:has-text("Filters")');
    await page.selectOption('select[name=sport]', 'soccer');
    await page.click('button:has-text("Apply Filters")');
    
    // Assert: Filtered results shown
    await expect(page.locator('text=Soccer')).toBeVisible();
  });
 
  test('user authentication flow', async ({ page }) => {
    // Act: Go to homepage
    await page.goto('/');
    
    // Assert: Not authenticated
    await expect(page.locator('text=Sign In')).toBeVisible();
 
    // Act: Navigate to login
    await page.click('text=Sign In');
    await expect(page).toHaveURL(/login/);
 
    // Act: Submit login form with invalid credentials
    await page.fill('input[name=email]', 'invalid@example.com');
    await page.fill('input[name=password]', 'wrongpassword');
    await page.click('button[type=submit]');
    
    // Assert: Error message shown
    await expect(page.locator('text=Invalid credentials')).toBeVisible();
 
    // Act: Try Google OAuth (mocked in test environment)
    await page.click('button:has-text("Continue with Google")');
    
    // Assert: Redirect to dashboard after successful auth
    await expect(page).toHaveURL(/dashboard/);
    await expect(page.locator('text=Dashboard')).toBeVisible();
  });
});

Security Considerations

Security is paramount in the frontend, with protections against common web vulnerabilities.

1. Content Security Policy (CSP)

File: next.config.js

Next.js headers implement strict CSP to prevent XSS attacks.

/** @type {import('next').NextConfig} */
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.googleapis.com https://unpkg.com https://esm.sh",
      "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
      "img-src 'self' data: https: blob:",
      "font-src 'self' https://fonts.gstatic.com",
      "connect-src 'self' https://api.altsportsleagues.ai wss://ws.altsportsleagues.ai https://*.googleapis.com",
      "frame-src 'self' https://*.google.com",
      "object-src 'none'",
    ].join('; '),
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
];
 
const nextConfig = {
  headers: async () => [
    {
      source: '/:path*',
      headers: securityHeaders,
    },
  ],
 
  // Other configurations
  reactStrictMode: true,
  swcMinify: true,
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
  eslint: {
    ignoreDuringBuilds: false,
  },
  typescript: {
    ignoreBuildErrors: false,
  },
};
 
module.exports = nextConfig;

2. Input Validation with Zod

Client-side validation complements backend Pydantic models.

File: src/lib/validations/league.ts

import { z } from 'zod';
 
export const leagueSchema = z.object({
  league_name: z
    .string()
    .min(3, 'League name must be at least 3 characters')
    .max(200, 'League name cannot exceed 200 characters')
    .regex(/^[A-Za-z\s\.\,\-\'\(\)]+$/, 'League name contains invalid characters'),
  website_url: z
    .string()
    .url('Please enter a valid URL')
    .optional()
    .or(z.literal('')),
  contact_email: z
    .string()
    .email('Please enter a valid email address')
    .optional()
    .or(z.literal('')),
  sport_bucket: z.enum(['combat', 'large_field', 'team', 'racing', 'other']),
  primary_location: z
    .string()
    .min(2, 'Location must be at least 2 characters')
    .max(100, 'Location cannot exceed 100 characters'),
});
 
export type LeagueFormData = z.infer<typeof leagueSchema>;

Deployment Configuration

Vercel Configuration

File: vercel.json

Vercel provides optimized hosting with automatic scaling and global CDN.

{
  "version": 2,
  "name": "altsportsleagues-frontend",
  "buildCommand": "npm run build",
  "outputDirectory": ".next",
  "framework": "nextjs",
  
  "env": {
    "NEXT_PUBLIC_API_URL": "@altsports-api-url",
    "NEXTAUTH_SECRET": "@nextauth-secret",
    "NEXTAUTH_URL": "@nextauth-url",
    "GOOGLE_CLIENT_ID": "@google-client-id",
    "GOOGLE_CLIENT_SECRET": "@google-client-secret"
  },
  
  "regions": ["iad1", "sfo1"],
  
  "functions": {
    "app/api/**/*.ts": {
      "maxDuration": 30,
      "memory": 1024,
      "maxInstances": 10
    }
  },
  
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        }
      ]
    }
  ],
  
  "rewrites": [
    {
      "source": "/api/(.*)",
      "destination": "http://localhost:8000/api/$1"
    }
  ]
}

Monitoring & Analytics

Vercel Analytics Integration

Built-in analytics track performance and user behavior.

// src/app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
import type { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: {
    default: 'AltSportsLeagues.ai',
    template: '%s | AltSportsLeagues.ai',
  },
  description: 'AI-powered sports league partnership intelligence',
};
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className="bg-background text-foreground">
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Sentry Error Tracking

Sentry provides comprehensive error monitoring and performance insights.

// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
 
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: process.env.NODE_ENV === 'development' ? 1.0 : 0.2,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  
  // Performance monitoring
  integrations: [
    new Sentry.BrowserTracing({
      routingInstrumentation: Sentry.reactRouterV6Instrumentation(
        useEffect,
        useLocation,
        useNavigationType
      ),
    }),
    new Sentry.Replay(),
  ],
  
  // Session grouping
  beforeSendTransaction: (transaction) => {
    const session = useSession();
    if (session.data?.user?.id) {
      transaction.setTag('user_id', session.data.user.id);
    }
    return transaction;
  },
});

Success Metrics

Performance Targets (Core Web Vitals)

  • Largest Contentful Paint (LCP): < 2.5 seconds for 95% of page loads
  • First Input Delay (FID): < 100 milliseconds for interactive elements
  • Cumulative Layout Shift (CLS): < 0.1 for visual stability
  • Time to Interactive (TTI): < 3.5 seconds for full interactivity
  • Total Blocking Time (TBT): < 300 milliseconds for smooth interactions

User Experience Targets

  • Accessibility Score: WCAG 2.1 AA compliance across all components
  • Mobile Lighthouse Score: > 90 for mobile performance and accessibility
  • Desktop Lighthouse Score: > 95 for desktop experience
  • Error Rate: < 0.1% of sessions encounter critical errors
  • User Retention: > 70% return within 7 days
  • Task Completion Rate: > 85% of workflows complete without assistance

Technical Targets

  • Bundle Size: Main bundle < 200KB, total < 1MB for above-the-fold
  • API Response Time: < 200ms for 95% of requests
  • Real-time Latency: < 100ms for WebSocket updates
  • Uptime: > 99.9% availability
  • Test Coverage: > 95% for critical paths, > 80% overall

This comprehensive Frontend Next.js Application documentation outlines the architecture, implementation patterns, and quality standards for AltSportsLeagues.ai's user interface. The design emphasizes performance, accessibility, and seamless integration with backend services to deliver an exceptional experience for league managers and business users.

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