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