diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.dockerignore b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.dockerignore new file mode 100644 index 000000000..2f30b0a76 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.dockerignore @@ -0,0 +1,62 @@ +node_modules +dist +.git +.gitignore +README.md +.env +.env.local +.env.development +.env.test +.env.production +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.vscode +.idea +*.swp +*.swo +*~ +.cursor +coverage +.nyc_output +.eslintcache +*.log +*.lock +*.tmp +*.tmp.* +log.txt + +.DS_Store +node_modules +**/node_modules/** +build +data +.env +load-ids.txt + +server +tmp +types +.git +.gitignore +dist +service +tests +fixtures-pages +fixtures-apps + +# Netlify +.netlify +packages/ml-air/lib +packages/ml-air/bin +packages/ml-air/project +packages/ml-air/share +packages/ml-air/random_forest_classification/ +packages/ml-air/__pycache__/ +packages/ml-air/app/__pycache__/ +packages/vcp-common/native-bridge/build +packages/vcp-common/_tests_/dataset-ranking.csv +node_modules/ +Dockerfile +.gitignore \ No newline at end of file diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.env b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.env new file mode 100644 index 000000000..6658458c6 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.env @@ -0,0 +1,6 @@ +# .env is better suited for public variables, ie, variables that should not commited +# For secret variables is better to use DevServerControl tool with set_env_variable: ["KEY", "SECRET"] + +# https://www.builder.io/c/docs/using-your-api-key +VITE_PUBLIC_BUILDER_KEY=__BUILDER_PUBLIC_KEY__ +PING_MESSAGE="ping pong" diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.gitignore b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.gitignore index a547bf36d..adf7425e9 100644 --- a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.gitignore +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +.config/ +!.env diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.npmrc b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.npmrc new file mode 100644 index 000000000..045129fcb --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true \ No newline at end of file diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.prettierrc b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.prettierrc new file mode 100644 index 000000000..d3be6d22e --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "trailingComma": "all" +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/AGENTS.md b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/AGENTS.md new file mode 100644 index 000000000..22b98d61c --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/AGENTS.md @@ -0,0 +1,164 @@ +# Fusion Starter + +A production-ready full-stack React application template with integrated Express server, featuring React Router 6 SPA mode, TypeScript, Vitest, Zod and modern tooling. + +While the starter comes with a express server, only create endpoint when strictly neccesary, for example to encapsulate logic that must leave in the server, such as private keys handling, or certain DB operations, db... + +## Tech Stack + +- **PNPM**: Prefer pnpm +- **Frontend**: React 18 + React Router 6 (spa) + TypeScript + Vite + TailwindCSS 3 +- **Backend**: Express server integrated with Vite dev server +- **Testing**: Vitest +- **UI**: Radix UI + TailwindCSS 3 + Lucide React icons + +## Project Structure + +``` +client/ # React SPA frontend +├── pages/ # Route components (Index.tsx = home) +├── components/ui/ # Pre-built UI component library +├── App.tsx # App entry point and with SPA routing setup +└── global.css # TailwindCSS 3 theming and global styles + +server/ # Express API backend +├── index.ts # Main server setup (express config + routes) +└── routes/ # API handlers + +shared/ # Types used by both client & server +└── api.ts # Example of how to share api interfaces +``` + +## Key Features + +## SPA Routing System + +The routing system is powered by React Router 6: + +- `client/pages/Index.tsx` represents the home page. +- Routes are defined in `client/App.tsx` using the `react-router-dom` import +- Route files are located in the `client/pages/` directory + +For example, routes can be defined with: + +```typescript +import { BrowserRouter, Routes, Route } from "react-router-dom"; + + + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> +; +``` + +### Styling System + +- **Primary**: TailwindCSS 3 utility classes +- **Theme and design tokens**: Configure in `client/global.css` +- **UI components**: Pre-built library in `client/components/ui/` +- **Utility**: `cn()` function combines `clsx` + `tailwind-merge` for conditional classes + +```typescript +// cn utility usage +className={cn( + "base-classes", + { "conditional-class": condition }, + props.className // User overrides +)} +``` + +### Express Server Integration + +- **Development**: Single port (8080) for both frontend/backend +- **Hot reload**: Both client and server code +- **API endpoints**: Prefixed with `/api/` + +#### Example API Routes +- `GET /api/ping` - Simple ping api +- `GET /api/demo` - Demo endpoint + +### Shared Types +Import consistent types in both client and server: +```typescript +import { DemoResponse } from '@shared/api'; +``` + +Path aliases: +- `@shared/*` - Shared folder +- `@/*` - Client folder + +## Development Commands + +```bash +pnpm dev # Start dev server (client + server) +pnpm build # Production build +pnpm start # Start production server +pnpm typecheck # TypeScript validation +pnpm test # Run Vitest tests +``` + +## Adding Features + +### Add new colors to the theme + +Open `client/global.css` and `tailwind.config.ts` and add new tailwind colors. + +### New API Route +1. **Optional**: Create a shared interface in `shared/api.ts`: +```typescript +export interface MyRouteResponse { + message: string; + // Add other response properties here +} +``` + +2. Create a new route handler in `server/routes/my-route.ts`: +```typescript +import { RequestHandler } from "express"; +import { MyRouteResponse } from "@shared/api"; // Optional: for type safety + +export const handleMyRoute: RequestHandler = (req, res) => { + const response: MyRouteResponse = { + message: 'Hello from my endpoint!' + }; + res.json(response); +}; +``` + +3. Register the route in `server/index.ts`: +```typescript +import { handleMyRoute } from "./routes/my-route"; + +// Add to the createServer function: +app.get("/api/my-endpoint", handleMyRoute); +``` + +4. Use in React components with type safety: +```typescript +import { MyRouteResponse } from '@shared/api'; // Optional: for type safety + +const response = await fetch('/api/my-endpoint'); +const data: MyRouteResponse = await response.json(); +``` + +### New Page Route +1. Create component in `client/pages/MyPage.tsx` +2. Add route in `client/App.tsx`: +```typescript +} /> +``` + +## Production Deployment + +- **Standard**: `pnpm build` +- **Binary**: Self-contained executables (Linux, macOS, Windows) +- **Cloud Deployment**: Use either Netlify or Vercel via their MCP integrations for easy deployment. Both providers work well with this starter template. + +## Architecture Notes + +- Single-port development with Vite + Express integration +- TypeScript throughout (client, server, shared) +- Full hot reload for rapid development +- Production-ready with multiple deployment options +- Comprehensive UI component library included +- Type-safe API communication via shared interfaces diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/OAUTH_SETUP.md b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/OAUTH_SETUP.md new file mode 100644 index 000000000..6ce099078 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/OAUTH_SETUP.md @@ -0,0 +1,153 @@ +# OAuth Authentication Setup + +This guide will help you set up Google and Apple OAuth authentication for your AFL Analytics application. + +## Prerequisites + +You need to create OAuth applications with Google and Apple to get the required credentials. + +## Google OAuth Setup + +### 1. Create a Google Cloud Project + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select an existing one +3. Enable the Google+ API and Google OAuth2 API + +### 2. Configure OAuth Consent Screen + +1. In the Google Cloud Console, go to "APIs & Services" > "OAuth consent screen" +2. Choose "External" user type +3. Fill in the required information: + - App name: AFL Analytics + - User support email: your email + - Developer contact information: your email +4. Add scopes: `userinfo.email` and `userinfo.profile` +5. Add test users if needed + +### 3. Create OAuth2 Credentials + +1. Go to "APIs & Services" > "Credentials" +2. Click "Create Credentials" > "OAuth 2.0 Client IDs" +3. Choose "Web application" +4. Set authorized redirect URIs: + - Development: `http://localhost:8080/api/auth/google/callback` + - Production: `https://yourdomain.com/api/auth/google/callback` +5. Save and copy the Client ID and Client Secret + +### 4. Update Environment Variables + +Set these environment variables in your application: + +```bash +GOOGLE_CLIENT_ID=your-actual-google-client-id +GOOGLE_CLIENT_SECRET=your-actual-google-client-secret +``` + +## Apple OAuth Setup + +### 1. Apple Developer Account + +You need an active Apple Developer Account to set up Sign In with Apple. + +### 2. Create an App ID + +1. Go to [Apple Developer Portal](https://developer.apple.com/account/) +2. Navigate to "Certificates, Identifiers & Profiles" > "Identifiers" +3. Create a new App ID with Sign In with Apple capability enabled + +### 3. Create a Services ID + +1. Create a new Services ID in the Apple Developer Portal +2. Enable "Sign In with Apple" +3. Configure the service: + - Primary App ID: Select the App ID you created + - Web Domain: Your domain (e.g., `localhost` for development) + - Return URLs: + - Development: `http://localhost:8080/api/auth/apple/callback` + - Production: `https://yourdomain.com/api/auth/apple/callback` + +### 4. Create a Private Key + +1. Go to "Keys" section in Apple Developer Portal +2. Create a new key with "Sign In with Apple" enabled +3. Download the private key file (.p8) +4. Note the Key ID + +### 5. Update Environment Variables + +Set these environment variables in your application: + +```bash +APPLE_CLIENT_ID=your-services-id +APPLE_TEAM_ID=your-team-id +APPLE_KEY_ID=your-key-id +APPLE_PRIVATE_KEY=your-private-key-content +``` + +## Setting Environment Variables in Builder.io + +To set these environment variables in your Builder.io project: + +1. **Important**: Use the DevServerControl tool to set environment variables (they won't be committed to git) +2. Or manually set them in your deployment environment + +Example for setting via DevServerControl: + +``` +GOOGLE_CLIENT_ID=your-actual-google-client-id +GOOGLE_CLIENT_SECRET=your-actual-google-client-secret +APPLE_CLIENT_ID=your-services-id +JWT_SECRET=your-super-secure-jwt-secret +``` + +## Testing OAuth + +### Google OAuth Test Flow + +1. Click "Continue with Google" button +2. You'll be redirected to Google's OAuth consent screen +3. Choose your Google account and grant permissions +4. You'll be redirected back and automatically logged in + +### Apple OAuth Test Flow + +1. Click "Continue with Apple" button +2. You'll be redirected to Apple's Sign In page +3. Enter your Apple ID credentials +4. Choose to share or hide your email +5. You'll be redirected back and automatically logged in + +## Security Notes + +- Never commit OAuth secrets to your repository +- Use different OAuth applications for development and production +- Regularly rotate your JWT secret +- Implement proper token expiration and refresh logic for production + +## Troubleshooting + +### Common Google OAuth Issues + +- **Invalid redirect_uri**: Ensure the redirect URI in Google Console matches exactly +- **Access blocked**: Add your domain to authorized domains in OAuth consent screen +- **API not enabled**: Enable Google+ API and OAuth2 API in Google Cloud Console + +### Common Apple OAuth Issues + +- **Invalid client**: Verify your Services ID is correctly configured +- **Invalid redirect**: Ensure redirect URIs match in Apple Developer Portal +- **Private key issues**: Ensure the private key is properly formatted + +### Debug Mode + +Set `NODE_ENV=development` to see detailed OAuth error logs in the server console. + +## Production Considerations + +1. Set up proper error handling for OAuth failures +2. Implement token refresh mechanisms +3. Add rate limiting to OAuth endpoints +4. Use HTTPS in production +5. Set secure cookie flags +6. Implement proper session management diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/README.md b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/README.md deleted file mode 100644 index 7059a962a..000000000 --- a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/App.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/App.tsx new file mode 100644 index 000000000..3a56b3cb4 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/App.tsx @@ -0,0 +1,96 @@ +import { Toaster } from "@/components/ui/toaster"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import ErrorBoundary from "@/components/ErrorBoundary"; +import Index from "./pages/Index"; +import Login from "./pages/Login"; +import AFLDashboard from "./pages/AFLDashboard"; +import PlayerPerformance from "./pages/PlayerPerformance"; +import CrowdMonitor from "./pages/CrowdMonitor"; +import Analytics from "./pages/Analytics"; +import Reports from "./pages/Reports"; +import ApiDiagnostics from "./pages/ApiDiagnostics"; +import ErrorDemo from "./pages/ErrorDemo"; +import NotFound from "./pages/NotFound"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: (failureCount, error) => { + try { + // Don't retry on 4xx errors + if (error && typeof error === "object" && "status" in error) { + const status = (error as any).status; + if (status >= 400 && status < 500) { + return false; + } + } + return failureCount < 3; + } catch (retryError) { + console.error("Error in retry logic:", retryError); + return false; + } + }, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + refetchOnWindowFocus: false, // Prevent unnecessary refetches that could cause errors + }, + mutations: { + retry: (failureCount, error) => { + try { + // Don't retry mutations on client errors + if (error && typeof error === "object" && "status" in error) { + const status = (error as any).status; + if (status >= 400 && status < 500) { + return false; + } + } + return failureCount < 2; // Fewer retries for mutations + } catch (retryError) { + console.error("Error in mutation retry logic:", retryError); + return false; + } + }, + }, + }, +}); + +export default function App() { + return ( + { + // In a real app, send this to your logging service + console.error("Global error caught:", error, errorInfo); + }} + > + + + + + + + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> + + + + + + ); +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/AFLPlayerCard.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/AFLPlayerCard.tsx new file mode 100644 index 000000000..9ef6fe6e9 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/AFLPlayerCard.tsx @@ -0,0 +1,346 @@ +import { useState } from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { cn } from "@/lib/utils"; +import { + Target, + Zap, + TrendingUp, + TrendingDown, + Award, + Activity, +} from "lucide-react"; + +interface PlayerStats { + disposals: number; + kicks: number; + handballs: number; + marks: number; + tackles: number; + goals: number; + behinds: number; + efficiency: number; + contested: number; + uncontested: number; + inside50s: number; + goalAccuracy: number; + avgSpeed: number; + maxSpeed: number; + distance: number; + clangers: number; + rebounds: number; + onePercenters: number; + turnovers: number; + intercepted: number; +} + +interface AFLPlayer { + id: number; + name: string; + team: string; + position: string; + number: number; + age: number; + height: string; + weight: string; + photo?: string; + stats: PlayerStats; + form: number[]; + heatMap: Array<{ + zone: string; + touches: number; + effectiveness: number; + }>; +} + +interface AFLPlayerCardProps { + player: AFLPlayer; + isSelected?: boolean; + onClick?: () => void; + className?: string; +} + +export default function AFLPlayerCard({ + player, + isSelected = false, + onClick, + className, +}: AFLPlayerCardProps) { + const [isFlipped, setIsFlipped] = useState(false); + + const handleCardClick = () => { + setIsFlipped(!isFlipped); + onClick?.(); + }; + + const getTeamColor = (team: string) => { + const teamColors: Record = { + "Western Bulldogs": "bg-blue-600", + Richmond: "bg-yellow-500", + Geelong: "bg-blue-800", + Melbourne: "bg-red-600", + Carlton: "bg-blue-500", + Adelaide: "bg-red-500", + "West Coast": "bg-blue-700", + Collingwood: "bg-black", + Essendon: "bg-red-700", + Fremantle: "bg-purple-600", + }; + return teamColors[team] || "bg-gray-600"; + }; + + const getPositionIcon = (position: string) => { + switch (position.toLowerCase()) { + case "midfielder": + return ; + case "forward": + return ; + case "defender": + return ; + case "ruckman": + return ; + default: + return ; + } + }; + + const recentForm = player.form?.slice(-3) || [80, 85, 90]; + const avgForm = recentForm.reduce((a, b) => a + b, 0) / recentForm.length; + const formTrend = recentForm[recentForm.length - 1] > recentForm[0]; + + return ( + +
+ {/* Front of card - Player Info */} +
+ + {/* Header with team colors */} +
+
+
#{player.number}
+ {getPositionIcon(player.position)} +
+
+
{player.team}
+
{player.position}
+
+
+ + {/* Player Photo Area */} +
+ {player.photo ? ( + {player.name} + ) : ( +
+ + {player.name + .split(" ") + .map((n) => n[0]) + .join("")} + +
+ )} +
+ + {/* Player Details */} +
+
+

+ {player.name} +

+

+ {player.age}y • {player.height} • {player.weight} +

+
+ + {/* Quick Stats */} +
+
+
+ {player.stats.goals} +
+
Goals
+
+
+
+ {player.stats.disposals} +
+
Disposals
+
+
+
+ {player.stats.efficiency}% +
+
Efficiency
+
+
+ + {/* Form indicator */} +
+ Form: + {formTrend ? ( + + ) : ( + + )} + = 85 ? "default" : "secondary"} + className="text-xs" + > + {avgForm.toFixed(0)} + +
+
+
+
+ + {/* Back of card - Performance Metrics */} +
+ +
+ {/* Header */} +
+

{player.name}

+

Performance Metrics

+
+ + {/* Performance Stats */} +
+ {/* Core Stats */} +
+

+ Core Stats +

+
+
+
{player.stats.kicks}
+
Kicks
+
+
+
+ {player.stats.handballs} +
+
Handballs
+
+
+
{player.stats.marks}
+
Marks
+
+
+
+ {player.stats.tackles} +
+
Tackles
+
+
+
+ + {/* Efficiency Meters */} +
+

+ Efficiency +

+
+
+
+ Overall + {player.stats.efficiency}% +
+ +
+
+
+ Goal Accuracy + {player.stats.goalAccuracy}% +
+ +
+
+
+ + {/* Movement Stats */} +
+

+ Movement +

+
+
+
+ {player.stats.avgSpeed} +
+
Avg Speed
+
+
+
+ {player.stats.maxSpeed} +
+
Max Speed
+
+
+
+ + {/* Recent Form */} +
+

+ Recent Form +

+
+ {recentForm.map((score, index) => ( +
= 90 + ? "bg-green-100 text-green-700" + : score >= 80 + ? "bg-yellow-100 text-yellow-700" + : "bg-red-100 text-red-700", + )} + > + {score} +
+ ))} +
+
+
+ + {/* Click hint */} +
+

Click to flip back

+
+
+
+
+
+
+ ); +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ErrorBoundary.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ErrorBoundary.tsx new file mode 100644 index 000000000..5a12d3b97 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ErrorBoundary.tsx @@ -0,0 +1,323 @@ +import React, { Component, ErrorInfo, ReactNode } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { + Activity, + Home, + RefreshCw, + Bug, + AlertTriangle, + Copy, + ExternalLink, +} from "lucide-react"; +import { Link } from "react-router-dom"; + +interface Props { + children?: ReactNode; + fallback?: ReactNode; + onError?: (error: Error, errorInfo: ErrorInfo) => void; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; + showDetails: boolean; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + errorInfo: null, + showDetails: false, + }; + + public static getDerivedStateFromError(error: Error): State { + return { + hasError: true, + error, + errorInfo: null, + showDetails: false, + }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("ErrorBoundary caught an error:", error, errorInfo); + + this.setState({ + error, + errorInfo, + }); + + // Call the onError callback if provided - wrap in try/catch to prevent cascading errors + try { + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + } catch (callbackError) { + console.error("Error in ErrorBoundary onError callback:", callbackError); + } + + // In a real app, you might want to send this to a logging service + // logErrorToService(error, errorInfo); + } + + private handleRetry = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + showDetails: false, + }); + }; + + private handleReload = () => { + window.location.reload(); + }; + + private copyErrorDetails = () => { + try { + const errorDetails = this.getErrorDetails(); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard + .writeText(errorDetails) + .then(() => { + console.log("Error details copied to clipboard"); + }) + .catch((error) => { + console.error("Failed to copy to clipboard:", error); + // Fallback: try to select text or show alert + this.fallbackCopy(errorDetails); + }); + } else { + this.fallbackCopy(errorDetails); + } + } catch (error) { + console.error("Copy operation failed:", error); + } + }; + + private fallbackCopy = (text: string) => { + try { + // Try to use the old execCommand method as fallback + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand("copy"); + console.log("Error details copied using fallback method"); + } finally { + if (textArea.parentNode) { + textArea.parentNode.removeChild(textArea); + } + } + } catch (fallbackError) { + console.error("Fallback copy failed:", fallbackError); + // Last resort: show alert with the text + alert("Copy failed. Here are the error details:\n\n" + text); + } + }; + + private getErrorDetails = () => { + const { error, errorInfo } = this.state; + return ` +AFL Analytics - Error Report +============================ +Time: ${new Date().toISOString()} +URL: ${window.location.href} +User Agent: ${navigator.userAgent} + +Error: ${error?.message || "Unknown error"} +Stack: ${error?.stack || "No stack trace"} + +Component Stack: ${errorInfo?.componentStack || "No component stack"} + `.trim(); + }; + + public render() { + if (this.state.hasError) { + // If a custom fallback is provided, use it + if (this.props.fallback) { + return this.props.fallback; + } + + // Default error UI + return ( +
+ {/* Header */} +
+
+ +
+ +
+ + AFL Analytics + + + Application Error +
+
+ + {/* Main Content */} +
+
+ + +
+ +
+ + Something Went Wrong + + + An unexpected error occurred in the application. Our team + has been notified. + +
+ + + + + Error:{" "} + {this.state.error?.message || "An unknown error occurred"} + + + +
+ + + +
+ + {/* Error Details Section */} +
+ + + {this.state.showDetails && ( +
+
+

+ Error Details +

+ +
+
+                          {this.getErrorDetails()}
+                        
+
+ )} +
+ + {/* Help Section */} +
+

+ Need Help? +

+
    +
  • + • Try refreshing the page or going back to the dashboard +
  • +
  • • Clear your browser cache and cookies
  • +
  • • Contact support if the error persists
  • +
  • + • Include the error details above when reporting the + issue +
  • +
+ +
+
+
+
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; + +// Higher-order component for wrapping components with error boundary +export function withErrorBoundary( + Component: React.ComponentType, + onError?: (error: Error, errorInfo: ErrorInfo) => void, +) { + return function WrappedComponent(props: T) { + return ( + + + + ); + }; +} + +// Hook for manual error reporting +export function useErrorHandler() { + return (error: Error, context?: string) => { + console.error("Manual error report:", { error, context }); + // In a real app, send to logging service + }; +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/LiveClock.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/LiveClock.tsx new file mode 100644 index 000000000..bc224ccd9 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/LiveClock.tsx @@ -0,0 +1,136 @@ +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Play, Pause, Clock } from "lucide-react"; + +interface LiveClockProps { + isLive: boolean; + onToggleLive: (isLive: boolean) => void; + matchTime?: { + quarter: number; + timeRemaining: string; + }; +} + +export default function LiveClock({ + isLive, + onToggleLive, + matchTime, +}: LiveClockProps) { + const [currentTime, setCurrentTime] = useState(new Date()); + const [gameTime, setGameTime] = useState({ + quarter: matchTime?.quarter || 2, + minutes: 15, + seconds: 23, + }); + + // Update current time every second + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(new Date()); + }, 1000); + + return () => clearInterval(timer); + }, []); + + // Update game time when live + useEffect(() => { + if (!isLive) return; + + const gameTimer = setInterval(() => { + setGameTime((prev) => { + let newSeconds = prev.seconds - 1; + let newMinutes = prev.minutes; + let newQuarter = prev.quarter; + + if (newSeconds < 0) { + newSeconds = 59; + newMinutes -= 1; + } + + if (newMinutes < 0) { + newMinutes = 19; + newSeconds = 59; + newQuarter += 1; + if (newQuarter > 4) { + newQuarter = 4; + newMinutes = 0; + newSeconds = 0; + } + } + + return { + quarter: newQuarter, + minutes: newMinutes, + seconds: newSeconds, + }; + }); + }, 1000); + + return () => clearInterval(gameTimer); + }, [isLive]); + + const formatTime = (time: Date) => { + return time.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + }; + + const formatGameTime = () => { + const mins = gameTime.minutes.toString().padStart(2, "0"); + const secs = gameTime.seconds.toString().padStart(2, "0"); + return `${mins}:${secs}`; + }; + + return ( +
+ {/* Live Status */} +
+ +
+ {isLive ? "LIVE" : "OFFLINE"} + + + +
+ + {/* Time Display */} +
+
+ + {formatTime(currentTime)} +
+ + {isLive && ( + <> +
+
+ Q{gameTime.quarter} + {formatGameTime()} +
+ + )} +
+
+ ); +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/LoadingState.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/LoadingState.tsx new file mode 100644 index 000000000..19f1dd73c --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/LoadingState.tsx @@ -0,0 +1,297 @@ +import { ReactNode } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Loader2, + RefreshCw, + AlertCircle, + Wifi, + Server, + Clock, + CheckCircle, +} from "lucide-react"; + +interface LoadingStateProps { + isLoading?: boolean; + error?: Error | string | null; + isEmpty?: boolean; + children?: ReactNode; + loadingText?: string; + emptyText?: string; + emptyDescription?: string; + onRetry?: () => void; + retryText?: string; + showRetry?: boolean; + variant?: "default" | "card" | "inline" | "skeleton"; + skeletonRows?: number; +} + +export default function LoadingState({ + isLoading = false, + error = null, + isEmpty = false, + children, + loadingText = "Loading...", + emptyText = "No data available", + emptyDescription = "There's nothing to show here yet.", + onRetry, + retryText = "Try Again", + showRetry = true, + variant = "default", + skeletonRows = 3, +}: LoadingStateProps) { + // Loading state + if (isLoading) { + if (variant === "skeleton") { + return ( +
+ {Array.from({ length: skeletonRows }).map((_, i) => ( +
+ + +
+ ))} +
+ ); + } + + if (variant === "inline") { + return ( +
+
+ + {loadingText} +
+
+ ); + } + + if (variant === "card") { + return ( + + + +

{loadingText}

+
+
+ ); + } + + // Default loading + return ( +
+ +

{loadingText}

+
+ ); + } + + // Error state + if (error) { + const errorMessage = typeof error === "string" ? error : error.message; + const isNetworkError = + errorMessage.toLowerCase().includes("network") || + errorMessage.toLowerCase().includes("fetch"); + const isServerError = + errorMessage.toLowerCase().includes("server") || + errorMessage.includes("5"); + + const ErrorIcon = isNetworkError + ? Wifi + : isServerError + ? Server + : AlertCircle; + const errorColor = isNetworkError + ? "text-blue-600" + : isServerError + ? "text-red-600" + : "text-orange-600"; + const bgColor = isNetworkError + ? "bg-blue-50 border-blue-200" + : isServerError + ? "bg-red-50 border-red-200" + : "bg-orange-50 border-orange-200"; + + if (variant === "inline") { + return ( + + + + {errorMessage} + {showRetry && onRetry && ( + + )} + + + ); + } + + if (variant === "card") { + return ( + + + +

+ {isNetworkError + ? "Connection Error" + : isServerError + ? "Server Error" + : "Error"} +

+

{errorMessage}

+ {showRetry && onRetry && ( + + )} +
+
+ ); + } + + // Default error + return ( +
+
+ +
+

+ {isNetworkError + ? "Connection Problem" + : isServerError + ? "Server Error" + : "Something went wrong"} +

+

{errorMessage}

+ {showRetry && onRetry && ( + + )} +
+
+
+ ); + } + + // Empty state + if (isEmpty) { + if (variant === "inline") { + return ( +
+

{emptyText}

+
+ ); + } + + if (variant === "card") { + return ( + + +
+ +
+

+ {emptyText} +

+

{emptyDescription}

+
+
+ ); + } + + // Default empty + return ( +
+
+ +
+

{emptyText}

+

{emptyDescription}

+
+ ); + } + + // Success state - render children + return <>{children}; +} + +// Specialized loading components +export const SkeletonLoader = ({ rows = 3 }: { rows?: number }) => ( + +); + +export const InlineLoader = ({ text = "Loading..." }: { text?: string }) => ( + +); + +export const CardLoader = ({ text = "Loading..." }: { text?: string }) => ( + +); + +// Data fetching wrapper component +interface DataWrapperProps { + data: any; + isLoading: boolean; + error: Error | string | null; + children: ReactNode; + emptyMessage?: string; + onRetry?: () => void; +} + +export const DataWrapper = ({ + data, + isLoading, + error, + children, + emptyMessage = "No data available", + onRetry, +}: DataWrapperProps) => { + const isEmpty = !data || (Array.isArray(data) && data.length === 0); + + return ( + + {children} + + ); +}; + +// Success state component +export const SuccessState = ({ + message = "Success!", + description, + action, +}: { + message?: string; + description?: string; + action?: ReactNode; +}) => ( +
+
+ +
+

{message}

+ {description &&

{description}

} + {action} +
+); diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/MobileNavigation.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/MobileNavigation.tsx new file mode 100644 index 000000000..0376f6ac3 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/MobileNavigation.tsx @@ -0,0 +1,218 @@ +import { useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { + Activity, + BarChart3, + Users, + Download, + Video, + Menu, + Home, + Zap, + Terminal, +} from "lucide-react"; + +const navigationItems = [ + { + name: "Home", + href: "/afl-dashboard", + icon: Home, + description: "Main dashboard", + }, + { + name: "Player Performance", + href: "/player-performance", + icon: BarChart3, + description: "Player stats & analysis", + }, + { + name: "Crowd Monitor", + href: "/crowd-monitor", + icon: Users, + description: "Stadium crowd analytics", + }, + { + name: "Analytics", + href: "/analytics", + icon: Video, + description: "Video analysis & reports", + }, + { + name: "Reports", + href: "/reports", + icon: Download, + description: "Download & manage reports", + }, + { + name: "API Diagnostics", + href: "/api-diagnostics", + icon: Terminal, + description: "System monitoring", + }, +]; + +export default function MobileNavigation() { + const [isOpen, setIsOpen] = useState(false); + const location = useLocation(); + + const isActive = (href: string) => { + if (href === "/") { + return location.pathname === "/"; + } + return location.pathname.startsWith(href); + }; + + return ( + <> + {/* Mobile Header */} +
+
+ +
+ +
+ + AFL Analytics + + + + + + + + +
+
+
+ +
+ AFL Analytics +
+ + +
+
+
+
+
+ + {/* Desktop Navigation */} + + + {/* Bottom Navigation for Mobile */} + + + ); +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/PlayerComparison.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/PlayerComparison.tsx new file mode 100644 index 000000000..647b2ceca --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/PlayerComparison.tsx @@ -0,0 +1,602 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + RadarChart, + Radar, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, + ResponsiveContainer, + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + LineChart, + Line, +} from "recharts"; +import { cn } from "@/lib/utils"; +import { ArrowLeftRight, BarChart3, TrendingUp, Award } from "lucide-react"; + +interface PlayerStats { + disposals: number; + kicks: number; + handballs: number; + marks: number; + tackles: number; + goals: number; + behinds: number; + efficiency: number; + contested: number; + uncontested: number; + inside50s: number; + goalAccuracy: number; + avgSpeed: number; + maxSpeed: number; + distance: number; + clangers: number; + rebounds: number; + onePercenters: number; + turnovers: number; + intercepted: number; +} + +interface AFLPlayer { + id: number; + name: string; + team: string; + position: string; + number: number; + age: number; + height: string; + weight: string; + stats: PlayerStats; + form: number[]; + heatMap: Array<{ + zone: string; + touches: number; + effectiveness: number; + }>; +} + +interface PlayerComparisonProps { + players: AFLPlayer[]; + selectedPlayer1?: AFLPlayer; + selectedPlayer2?: AFLPlayer; + onPlayerSelect?: (player: AFLPlayer, position: 1 | 2) => void; +} + +export default function PlayerComparison({ + players, + selectedPlayer1, + selectedPlayer2, + onPlayerSelect, +}: PlayerComparisonProps) { + const [comparisonType, setComparisonType] = useState< + "radar" | "bar" | "form" + >("radar"); + + const player1 = selectedPlayer1 || players[0]; + const player2 = selectedPlayer2 || players[1]; + + // Prepare radar chart data + const radarData = [ + { + stat: "Goals", + player1: player1.stats.goals, + player2: player2.stats.goals, + maxValue: Math.max(player1.stats.goals, player2.stats.goals, 5), + }, + { + stat: "Disposals", + player1: Math.round((player1.stats.disposals / 50) * 100), + player2: Math.round((player2.stats.disposals / 50) * 100), + maxValue: 100, + }, + { + stat: "Marks", + player1: Math.round((player1.stats.marks / 15) * 100), + player2: Math.round((player2.stats.marks / 15) * 100), + maxValue: 100, + }, + { + stat: "Tackles", + player1: Math.round((player1.stats.tackles / 15) * 100), + player2: Math.round((player2.stats.tackles / 15) * 100), + maxValue: 100, + }, + { + stat: "Efficiency", + player1: player1.stats.efficiency, + player2: player2.stats.efficiency, + maxValue: 100, + }, + { + stat: "Speed", + player1: Math.round((player1.stats.avgSpeed / 35) * 100), + player2: Math.round((player2.stats.avgSpeed / 35) * 100), + maxValue: 100, + }, + ]; + + // Prepare bar chart data + const barData = [ + { + stat: "Goals", + player1: player1.stats.goals, + player2: player2.stats.goals, + }, + { + stat: "Disposals", + player1: player1.stats.disposals, + player2: player2.stats.disposals, + }, + { + stat: "Marks", + player1: player1.stats.marks, + player2: player2.stats.marks, + }, + { + stat: "Tackles", + player1: player1.stats.tackles, + player2: player2.stats.tackles, + }, + { + stat: "Kicks", + player1: player1.stats.kicks, + player2: player2.stats.kicks, + }, + { + stat: "Handballs", + player1: player1.stats.handballs, + player2: player2.stats.handballs, + }, + ]; + + // Prepare form comparison data + const formData = Array.from( + { length: Math.max(player1.form.length, player2.form.length) }, + (_, i) => ({ + game: `Game ${i + 1}`, + player1: player1.form[i] || 0, + player2: player2.form[i] || 0, + }), + ); + + const getTeamColor = (team: string) => { + const teamColors: Record = { + "Western Bulldogs": "#1E40AF", + Richmond: "#EAB308", + Geelong: "#1E3A8A", + Melbourne: "#DC2626", + Carlton: "#3B82F6", + Adelaide: "#EF4444", + "West Coast": "#1D4ED8", + Collingwood: "#000000", + Essendon: "#B91C1C", + Fremantle: "#9333EA", + }; + return teamColors[team] || "#6B7280"; + }; + + const swapPlayers = () => { + if (onPlayerSelect) { + onPlayerSelect(player2, 1); + onPlayerSelect(player1, 2); + } + }; + + const StatComparison = ({ + label, + value1, + value2, + unit = "", + }: { + label: string; + value1: number; + value2: number; + unit?: string; + }) => { + const max = Math.max(value1, value2); + const winner1 = value1 > value2; + const winner2 = value2 > value1; + const tie = value1 === value2; + + return ( +
+
+ {label} +
+ + {value1} + {unit} + + vs + + {value2} + {unit} + +
+
+
+
+
+
+
+
+
+
+
+ ); + }; + + return ( +
+ {/* Player Selection */} + + + + + Player Comparison + + + +
+ {/* Player 1 Selection */} +
+ + +
+ + {/* Player 2 Selection */} +
+ + +
+
+ +
+ +
+
+
+ + {/* Player Cards */} +
+ {/* Player 1 Card */} + + +
+
+ #{player1.number} +
+
+ {player1.name} +

+ {player1.team} • {player1.position} +

+
+
+
+ +
+
+
+ {player1.stats.goals} +
+
Goals
+
+
+
+ {player1.stats.disposals} +
+
Disposals
+
+
+
+ {player1.stats.efficiency}% +
+
Efficiency
+
+
+
+
+ + {/* Player 2 Card */} + + +
+
+ #{player2.number} +
+
+ {player2.name} +

+ {player2.team} • {player2.position} +

+
+
+
+ +
+
+
+ {player2.stats.goals} +
+
Goals
+
+
+
+ {player2.stats.disposals} +
+
Disposals
+
+
+
+ {player2.stats.efficiency}% +
+
Efficiency
+
+
+
+
+
+ + {/* Comparison Chart */} + + +
+ + + Performance Comparison + +
+ + + +
+
+
+ +
+ {comparisonType === "radar" && ( + + + + + + + + + + + )} + + {comparisonType === "bar" && ( + + + + + + + + + + + + )} + + {comparisonType === "form" && ( + + + + + + + + + + + + )} +
+
+
+ + {/* Detailed Stat Comparison */} + + + + + Detailed Statistics + + + + + + + + + + + + + + + +
+ ); +} diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/accordion.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/accordion.tsx new file mode 100644 index 000000000..83ff01790 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/alert-dialog.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/alert-dialog.tsx new file mode 100644 index 000000000..235001462 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/alert.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/alert.tsx new file mode 100644 index 000000000..13219e774 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/aspect-ratio.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/aspect-ratio.tsx new file mode 100644 index 000000000..c9e6f4bf9 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/avatar.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/avatar.tsx new file mode 100644 index 000000000..444b1dbaa --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/badge.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/badge.tsx new file mode 100644 index 000000000..d3d5d6040 --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/breadcrumb.tsx b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/breadcrumb.tsx new file mode 100644 index 000000000..6934f83bd --- /dev/null +++ b/Player_Tracking/afl_player_tracking_and_crowd_monitoring/frontend/client/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>