├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── SECURITY.md ├── bun.lockb ├── components.json ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── favicon.ico ├── placeholder.svg └── robots.txt ├── src ├── App.css ├── App.tsx ├── app │ └── globals.css ├── components │ ├── BrakeChart.tsx │ ├── CircuitComparisonChart.tsx │ ├── DRSChart.tsx │ ├── DiscordCommunityBanner.tsx │ ├── DriverComparison.tsx │ ├── DriverComparisonTelemetry.tsx │ ├── F1Card.tsx │ ├── FeatureAnnouncementBanner.tsx │ ├── FloatingNavbar.tsx │ ├── Footer.tsx │ ├── GearMapChart.tsx │ ├── KeyMomentsHighlight.tsx │ ├── LandingFooter.tsx │ ├── MobileWarningBanner.tsx │ ├── Navbar.tsx │ ├── PositionChart.tsx │ ├── PositionsSummaryTable.tsx │ ├── PositionsTabContent.tsx │ ├── RPMChart.tsx │ ├── RacingChart.tsx │ ├── ScrollToTop.tsx │ ├── SpeedTraceChart.tsx │ ├── StintAnalysisTable.tsx │ ├── TeamPaceTable.tsx │ ├── ThrottleChart.tsx │ ├── TireStrategy.tsx │ ├── TrackEvolutionChart.tsx │ ├── TrackProgress.tsx │ └── ui │ │ ├── LoadingSpinnerF1.tsx │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── contexts │ ├── AuthContext.tsx │ └── SeasonContext.tsx ├── data │ └── mockData.ts ├── hooks │ ├── use-mobile.tsx │ └── use-toast.ts ├── index.css ├── lib │ ├── api.ts │ ├── driverColor.ts │ ├── teamUtils.ts │ └── utils.ts ├── main.tsx ├── pages │ ├── Dashboard.tsx │ ├── DriverDetailsPage.tsx │ ├── DriverStandings.tsx │ ├── FAQ.tsx │ ├── Landing.tsx │ ├── NotFound.tsx │ ├── PrivacyPolicy.tsx │ ├── Race.tsx │ ├── Races.tsx │ ├── TeamDetailsPage.tsx │ ├── TeamStandings.tsx │ └── TermsOfService.tsx ├── types.d.ts ├── utils │ └── donationPopupUtils.ts └── vite-env.d.ts ├── tailwind.config.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: subhashhhhhh # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: fastlytics 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: subhashh 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # Testing 10 | /coverage 11 | 12 | # Vite / Build Output 13 | /dist 14 | /dist-ssr 15 | /.vite 16 | *.local 17 | 18 | # TypeScript 19 | *.tsbuildinfo 20 | 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | pnpm-debug.log* 28 | lerna-debug.log* 29 | 30 | # Environment Variables 31 | .env* 32 | !.env.example 33 | 34 | # Editor directories and files 35 | .vscode/* 36 | !.vscode/extensions.json 37 | .idea 38 | *.suo 39 | *.ntvs* 40 | *.njsproj 41 | *.sln 42 | *.sw? 43 | 44 | # OS generated files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional PNPM lockfile (if applicable, but usually committed) 55 | # pnpm-lock.yaml 56 | 57 | # Backend API Directory (Specific Rules) 58 | # Ignore the entire directory by default 59 | /backend-api/ 60 | # But DO NOT ignore a .gitignore file within it, allowing specific un-ignores 61 | !/backend-api/.gitignore 62 | # Ignore environment files within the backend directory 63 | /backend-api/.env* 64 | # Ignore Python cache/compiled files 65 | /backend-api/cache/ 66 | /backend-api/__pycache__/ 67 | /backend-api/*.pyc 68 | /.kilocode/ 69 | 70 | # Wiki documentation (not tracked in Git) 71 | WIKI.md 72 | SECURITY.md 73 | 74 | # Blog post about building Fastlytics 75 | building-fastlytics.md 76 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Subhash Gottumukkala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏎️ Fastlytics 2 | 3 | *A lightning-fast platform for Formula 1 fans to explore historical data, compare drivers, and simulate race strategies – no engineering degree required.* 4 | 5 | 6 | ## 🚀 Features 7 | 8 | ### 🏁 **Core Features** 9 | - **Lap Time Comparisons**: Compare drivers’ laps side-by-side (e.g., *Hamilton vs. Verstappen at Monaco*). 10 | - **Gear Shift Visualization**: See how drivers attack corners with animated gear shift maps. 11 | - **Tire Strategy Breakdowns**: Analyze pit stop efficiency and compound performance. 12 | - **Position Change Graphs**: Relive epic battles with lap-by-lap position swings. 13 | - **Track Evolution Analysis**: Watch how lap times drop as rubber builds up on the circuit. 14 | 15 | --- 16 | 17 | ## 🛠️ Tech Stack 18 | 19 | | **Category** | **Tools** | 20 | |---------------------|---------------------------------------------------------------------------| 21 | | **Frontend** | React, Tailwind CSS, shadcn/ui, Lucide React, Custom Charts | 22 | | **Backend** | Fast-F1 API, Supabase (Auth + PostgreSQL), Cloudflare R2 (Storage) | 23 | | **Infrastructure** | Docker, Oracle, Redis (Caching) | 24 | | **Misc** | Python (Data Processing), | 25 | 26 | --- 27 | 28 | ## ⚡ Quick Start 29 | 30 | ### Prerequisites 31 | 32 | - Node.js v18+ 33 | - Python 3.10+ 34 | - Docker (for local Supabase/PostgreSQL) 35 | 36 | ### Installation 37 | 38 | 1. Clone the repository: 39 | ```bash 40 | git clone https://github.com/subhashhhhhh/Fastlytics.git 41 | ``` 42 | 43 | 2. Install frontend dependencies: 44 | ```bash 45 | npm install 46 | ``` 47 | 48 | 3. Install backend dependencies: 49 | ```bash 50 | git clone https://github.com/subhashhhhhh/Fastlytics-Backend.git 51 | pip install -r requirements.txt 52 | ``` 53 | 54 | ### Environment Setup 55 | 56 | 1. Configure environment variables: 57 | ```bash 58 | cp .env.example .env 59 | ``` 60 | 61 | 2. Configure backend environment variables: 62 | ```bash 63 | cd backend-api 64 | cp .env.example .env 65 | ``` 66 | 67 | ## 🤝 Contributing 68 | **We welcome pit crew members!** 69 | 1. Fork the repository. 70 | 2. Create a branch: `git checkout -b feature/brazilian-gp-2023`. 71 | 3. Commit changes: `git commit -m "Added Hamilton’s magic telemetry"`. 72 | 4. Push: `git push origin feature/brazilian-gp-2023`. 73 | 5. Submit a PR. 74 | 75 | *No toxic rivalries allowed – this is a Ferrari/Mercedes/Red Bull neutral zone.* 🏳️ 76 | 77 | --- 78 | 79 | ## 📜 License 80 | MIT License – *Do whatever you want, but don’t blame us if your AI predicts Stroll as 2024 champion.* 81 | 82 | --- 83 | 84 | ## 🙏 Acknowledgments 85 | - **Fast-F1**: For the incredible Python library that makes this possible. 86 | - **Supabase**: For auth and database. 87 | - **You**: For not asking why we included the 2022 Ferrari strategy engine. 88 | 89 | --- 90 | 91 | *Built with ❤️ and excessive caffeine by Subhash Gottumukkala.* 92 | 93 | --- 94 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | I take the security of Fastlytics seriously. If you believe you've found a security vulnerability, please follow these steps: 6 | 7 | 1. **Do Not** disclose the vulnerability publicly on GitHub Issues or any public forum 8 | 2. Email me at [contact@fastlytics.app](mailto:contact@fastlytics.app) with details about the vulnerability 9 | 3. Include the following information in your report: 10 | - Description of the vulnerability 11 | - Steps to reproduce 12 | - Potential impact 13 | - Suggested fix (if any) 14 | 15 | ### Response Timeline 16 | 17 | - You will receive an acknowledgment of your report within 48 hours 18 | - I aim to validate and assess the severity of each report within 7 days 19 | - I will work on a fix based on severity: 20 | - Critical: 24-48 hours 21 | - High: 1 week 22 | - Medium: 2 weeks 23 | - Low: Next release cycle 24 | 25 | ### What to Expect 26 | 27 | - A timely response to your report 28 | - Regular updates on our progress addressing the issue 29 | - Credit for discovering the vulnerability (unless you request anonymity) 30 | - Notification when the vulnerability is fixed 31 | 32 | ## Security Best Practices 33 | 34 | ### For Developers 35 | 36 | 1. **API Keys**: Never commit API keys or other secrets to GitHub 37 | 2. **Authentication**: Use Supabase authentication as documented 38 | 3. **Data Handling**: Sanitize all user inputs, especially when processing custom race/driver data 39 | 4. **Dependencies**: Keep dependencies updated and regularly run security audits 40 | 41 | ### For Users 42 | 43 | 1. **Strong Passwords**: Use strong, unique passwords for your Fastlytics account 44 | 45 | ## Security Features 46 | 47 | Fastlytics employs several security measures: 48 | 49 | 1. **API Key Authentication**: Backend API endpoints are protected by API keys 50 | 2. **Authentication**: User authentication through Supabase 51 | 3. **Sanitized Inputs**: All user inputs are sanitized before processing 52 | 4. **Secure Data Storage**: Sensitive data is stored securely in Supabase/Cloudflare R2 53 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subhashhhhhh/Fastlytics/ba7211c7c7701e66964c94ccadee1ec010e4a082/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | "@typescript-eslint/no-unused-vars": "off", 27 | }, 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fastlytics 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite_react_shadcn_ts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build:dev": "vite build --mode development", 10 | "lint": "eslint .", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@heroicons/react": "^2.2.0", 15 | "@hookform/resolvers": "^3.9.0", 16 | "@radix-ui/react-accordion": "^1.2.0", 17 | "@radix-ui/react-alert-dialog": "^1.1.1", 18 | "@radix-ui/react-aspect-ratio": "^1.1.0", 19 | "@radix-ui/react-avatar": "^1.1.0", 20 | "@radix-ui/react-checkbox": "^1.1.1", 21 | "@radix-ui/react-collapsible": "^1.1.0", 22 | "@radix-ui/react-context-menu": "^2.2.1", 23 | "@radix-ui/react-dialog": "^1.1.2", 24 | "@radix-ui/react-dropdown-menu": "^2.1.1", 25 | "@radix-ui/react-hover-card": "^1.1.1", 26 | "@radix-ui/react-label": "^2.1.0", 27 | "@radix-ui/react-menubar": "^1.1.1", 28 | "@radix-ui/react-navigation-menu": "^1.2.0", 29 | "@radix-ui/react-popover": "^1.1.1", 30 | "@radix-ui/react-progress": "^1.1.0", 31 | "@radix-ui/react-radio-group": "^1.2.0", 32 | "@radix-ui/react-scroll-area": "^1.1.0", 33 | "@radix-ui/react-select": "^2.1.1", 34 | "@radix-ui/react-separator": "^1.1.0", 35 | "@radix-ui/react-slider": "^1.2.0", 36 | "@radix-ui/react-slot": "^1.1.0", 37 | "@radix-ui/react-switch": "^1.1.0", 38 | "@radix-ui/react-tabs": "^1.1.0", 39 | "@radix-ui/react-toast": "^1.2.1", 40 | "@radix-ui/react-toggle": "^1.1.0", 41 | "@radix-ui/react-toggle-group": "^1.1.0", 42 | "@radix-ui/react-tooltip": "^1.1.4", 43 | "@supabase/supabase-js": "^2.49.4", 44 | "@tanstack/react-query": "^5.56.2", 45 | "@tanstack/react-table": "^8.21.2", 46 | "@tremor/react": "^3.18.7", 47 | "class-variance-authority": "^0.7.1", 48 | "clsx": "^2.1.1", 49 | "cmdk": "^1.0.0", 50 | "date-fns": "^3.6.0", 51 | "embla-carousel-react": "^8.3.0", 52 | "framer-motion": "^12.8.0", 53 | "html-to-image": "^1.11.13", 54 | "input-otp": "^1.2.4", 55 | "lucide-react": "^0.462.0", 56 | "next-themes": "^0.3.0", 57 | "react": "^18.3.1", 58 | "react-day-picker": "^8.10.1", 59 | "react-dom": "^18.3.1", 60 | "react-hook-form": "^7.53.0", 61 | "react-resizable-panels": "^2.1.3", 62 | "react-router-dom": "^6.26.2", 63 | "recharts": "^2.12.7", 64 | "simple-icons": "^14.12.0", 65 | "sonner": "^1.5.0", 66 | "tailwind-merge": "^2.5.2", 67 | "tailwindcss-animate": "^1.0.7", 68 | "vaul": "^0.9.3", 69 | "zod": "^3.23.8" 70 | }, 71 | "devDependencies": { 72 | "@eslint/js": "^9.9.0", 73 | "@tailwindcss/typography": "^0.5.15", 74 | "@types/node": "^22.5.5", 75 | "@types/react": "^18.3.3", 76 | "@types/react-dom": "^18.3.0", 77 | "@vitejs/plugin-react-swc": "^3.5.0", 78 | "autoprefixer": "^10.4.20", 79 | "eslint": "^9.9.0", 80 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 81 | "eslint-plugin-react-refresh": "^0.4.9", 82 | "globals": "^15.9.0", 83 | "lovable-tagger": "^1.1.7", 84 | "postcss": "^8.4.47", 85 | "tailwindcss": "^3.4.11", 86 | "typescript": "^5.5.3", 87 | "typescript-eslint": "^8.0.1", 88 | "vite": "^6.2.6" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/subhashhhhhh/Fastlytics/ba7211c7c7701e66964c94ccadee1ec010e4a082/public/favicon.ico -------------------------------------------------------------------------------- /public/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: Googlebot 2 | Allow: / 3 | 4 | User-agent: Bingbot 5 | Allow: / 6 | 7 | User-agent: Twitterbot 8 | Allow: / 9 | 10 | User-agent: facebookexternalhit 11 | Allow: / 12 | 13 | User-agent: * 14 | Allow: / 15 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "@/components/ui/toaster"; 2 | import { Toaster as Sonner } from "@/components/ui/sonner"; 3 | import { TooltipProvider } from "@/components/ui/tooltip"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 6 | import Dashboard from "./pages/Dashboard"; 7 | import Landing from "./pages/Landing"; // Updated import 8 | import Race from "./pages/Race"; 9 | import TeamStandings from "./pages/TeamStandings"; 10 | import DriverStandings from "./pages/DriverStandings"; 11 | import Races from "./pages/Races"; 12 | import NotFound from "./pages/NotFound"; 13 | import PrivacyPolicy from "./pages/PrivacyPolicy"; 14 | import TermsOfService from "./pages/TermsOfService"; 15 | import FAQ from "./pages/FAQ"; 16 | import Footer from "./components/Footer"; 17 | import LandingFooter from "./components/LandingFooter"; 18 | import { SeasonProvider } from "./contexts/SeasonContext"; 19 | import { AuthProvider } from "./contexts/AuthContext"; 20 | import ScrollToTop from "@/components/ScrollToTop"; 21 | 22 | const queryClient = new QueryClient(); 23 | 24 | // Layout component to add footer to pages 25 | const MainLayout = ({ children }: { children: React.ReactNode }) => ( 26 |
27 | {children} 28 |
30 | ); 31 | 32 | // Landing layout with custom footer 33 | const LandingLayout = ({ children }: { children: React.ReactNode }) => ( 34 |
35 | {children} 36 | 37 |
38 | ); 39 | 40 | const App = () => ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {/* Landing Page Route (with LandingFooter) */} 51 | } /> 52 | 53 | {/* Public Info Pages */} 54 | } /> 55 | } /> 56 | } /> 57 | 58 | {/* All routes are now public */} 59 | } /> 60 | } /> 61 | } /> 62 | } /> 63 | } /> 64 | 65 | {/* 404 page */} 66 | } /> 67 | 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | /* Custom scrollbar for dropdowns */ 2 | .custom-scrollbar::-webkit-scrollbar { 3 | width: 6px; 4 | height: 6px; 5 | } 6 | 7 | .custom-scrollbar::-webkit-scrollbar-track { 8 | background: rgba(31, 41, 55, 0.3); 9 | border-radius: 8px; 10 | } 11 | 12 | .custom-scrollbar::-webkit-scrollbar-thumb { 13 | background: rgba(239, 68, 68, 0.3); 14 | border-radius: 8px; 15 | } 16 | 17 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 18 | background: rgba(239, 68, 68, 0.5); 19 | } 20 | 21 | /* Firefox scrollbar */ 22 | .custom-scrollbar { 23 | scrollbar-width: thin; 24 | scrollbar-color: rgba(239, 68, 68, 0.3) rgba(31, 41, 55, 0.3); 25 | } -------------------------------------------------------------------------------- /src/components/DiscordCommunityBanner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ArrowRight, Users, MessageCircle } from 'lucide-react'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface DiscordCommunityBannerProps { 6 | className?: string; 7 | } 8 | 9 | const DiscordCommunityBanner: React.FC = ({ 10 | className 11 | }) => { 12 | return ( 13 |
17 |
18 | {/* Accurate Discord logo SVG */} 19 |
20 | 28 | 29 | 30 |
31 | 32 |
33 |

Join Our Discord Community!

34 |

Connect with other F1 fans, discuss races, share analysis, and get the latest updates from the Fastlytics team. We're building a community of passionate Formula 1 fans who love data.

35 | 36 |
37 |
38 | 39 | 100+ Members 40 |
41 |
42 | 43 | Active Discussions 44 |
45 | 51 | Join now 52 | 53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | export default DiscordCommunityBanner; -------------------------------------------------------------------------------- /src/components/DriverComparison.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { 4 | ResponsiveContainer, 5 | RadarChart, 6 | PolarGrid, 7 | PolarAngleAxis, 8 | PolarRadiusAxis, 9 | Radar, 10 | Legend 11 | } from 'recharts'; 12 | import { cn } from "@/lib/utils"; 13 | 14 | interface DriverComparisonProps { 15 | data: any[]; 16 | className?: string; 17 | delay?: number; 18 | } 19 | 20 | const DriverComparison: React.FC = ({ 21 | data, 22 | className, 23 | delay = 0 24 | }) => { 25 | // Get the driver ids from the first data item (excluding 'attribute') 26 | const drivers = Object.keys(data[0] || {}).filter(key => key !== 'attribute'); 27 | 28 | // Define colors for drivers 29 | const driverColors: Record = { 30 | verstappen: '#1e41ff', // Red Bull blue 31 | hamilton: '#6cd3bf', // Mercedes teal 32 | leclerc: '#ff2800', // Ferrari red 33 | norris: '#ff8700', // McLaren orange 34 | russell: '#6cd3bf', // Mercedes teal 35 | sainz: '#ff2800', // Ferrari red 36 | piastri: '#ff8700', // McLaren orange 37 | perez: '#1e41ff', // Red Bull blue 38 | }; 39 | 40 | return ( 41 |
45 |

Driver Performance Comparison

46 | 47 | 48 | 49 | 53 | 54 | 55 | {drivers.map(driver => ( 56 | 64 | ))} 65 | 66 | 67 | 68 | 69 |
70 | ); 71 | }; 72 | 73 | export default DriverComparison; 74 | -------------------------------------------------------------------------------- /src/components/F1Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from "@/lib/utils"; 3 | import { ArrowUp, ArrowDown, MinusCircle } from 'lucide-react'; // Import icons 4 | 5 | interface F1CardProps { 6 | title: string; 7 | value: string | number; 8 | team: 'ferrari' | 'mercedes' | 'mclaren' | 'redbull' | 'astonmartin' | 'alpine' | 'williams' | 'haas' | 'alfaromeo' | 'alphatauri' | 'gray'; // Added missing teams + gray 9 | icon?: React.ReactNode; 10 | points_change?: number; // Use points_change instead of change 11 | subValue?: string | null; // Optional sub-value (e.g., lap time) 12 | className?: string; 13 | style?: React.CSSProperties; 14 | isRookie?: boolean; // Add isRookie prop 15 | } 16 | 17 | const F1Card = ({ title, value, team, icon, points_change, subValue, className, style, isRookie }: F1CardProps) => { 18 | 19 | // Function to determine change indicator color and icon (copied from standings pages) 20 | const getChangeIndicator = (change: number | undefined) => { 21 | if (change === undefined) { 22 | return null; 23 | } 24 | if (change > 0) { 25 | return { color: 'text-green-500', icon: }; 26 | } else if (change < 0) { 27 | return { color: 'text-red-500', icon: }; 28 | } else { // change === 0 29 | return { color: 'text-gray-500', icon: }; 30 | } 31 | }; 32 | 33 | const indicator = getChangeIndicator(points_change); 34 | 35 | return ( 36 |
{/* Use team prop for styling */} 37 |
38 |
39 |
40 |

{title}

41 | {isRookie && ( 42 | 43 | Rookie 44 | 45 | )} 46 |
47 |
{value}
48 | {/* Display subValue (lap time) if available */} 49 | {subValue && ( 50 |
{subValue}
51 | )} 52 | 53 | {/* Display points change indicator if available */} 54 | {indicator && ( 55 |
56 | {indicator.icon} 57 | {points_change !== 0 ? Math.abs(points_change ?? 0) : '-'} 58 |
59 | )} 60 |
61 | 62 | {icon && ( 63 |
64 | {icon} 65 |
66 | )} 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default F1Card; 73 | -------------------------------------------------------------------------------- /src/components/FeatureAnnouncementBanner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Sparkles, X, ArrowRight } from 'lucide-react'; 3 | import { Link } from 'react-router-dom'; 4 | import { cn } from '@/lib/utils'; 5 | 6 | interface FeatureAnnouncementBannerProps { 7 | id: string; // Unique identifier for this announcement 8 | title: string; 9 | message: string; 10 | linkText?: string; 11 | linkHref?: string; 12 | expiresInDays?: number; // How many days the banner should show 13 | className?: string; 14 | } 15 | 16 | const FeatureAnnouncementBanner: React.FC = ({ 17 | id, 18 | title, 19 | message, 20 | linkText, 21 | linkHref, 22 | expiresInDays = 7, 23 | className 24 | }) => { 25 | const [isVisible, setIsVisible] = useState(false); 26 | 27 | useEffect(() => { 28 | // Check if this announcement has been dismissed 29 | const dismissedAnnouncements = JSON.parse(localStorage.getItem('dismissedAnnouncements') || '{}'); 30 | 31 | // If there's an expiry date stored, check if it's passed 32 | if (dismissedAnnouncements[id]) { 33 | const expiryDate = new Date(dismissedAnnouncements[id]); 34 | if (new Date() > expiryDate) { 35 | // Expired, show again 36 | delete dismissedAnnouncements[id]; 37 | localStorage.setItem('dismissedAnnouncements', JSON.stringify(dismissedAnnouncements)); 38 | setIsVisible(true); 39 | } else { 40 | // Not expired, keep hidden 41 | setIsVisible(false); 42 | } 43 | } else { 44 | // Never dismissed, show it 45 | setIsVisible(true); 46 | } 47 | }, [id, expiresInDays]); 48 | 49 | const dismissAnnouncement = () => { 50 | const dismissedAnnouncements = JSON.parse(localStorage.getItem('dismissedAnnouncements') || '{}'); 51 | 52 | // Set expiry date 53 | const expiryDate = new Date(); 54 | expiryDate.setDate(expiryDate.getDate() + expiresInDays); 55 | 56 | dismissedAnnouncements[id] = expiryDate.toISOString(); 57 | localStorage.setItem('dismissedAnnouncements', JSON.stringify(dismissedAnnouncements)); 58 | 59 | setIsVisible(false); 60 | }; 61 | 62 | if (!isVisible) return null; 63 | 64 | return ( 65 |
66 |
67 | 68 |
69 |

{title}

70 |

{message}

71 | {linkText && linkHref && ( 72 | 76 | {linkText} 77 | 78 | )} 79 |
80 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default FeatureAnnouncementBanner; -------------------------------------------------------------------------------- /src/components/FloatingNavbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button } from "@/components/ui/button"; 4 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 5 | import { Menu, Gauge, ChevronRight } from 'lucide-react'; 6 | import { motion } from 'framer-motion'; 7 | 8 | interface FloatingNavbarProps { 9 | transparent?: boolean; 10 | } 11 | 12 | const FloatingNavbar: React.FC = ({ transparent = false }) => { 13 | const [scrollPosition, setScrollPosition] = useState(0); 14 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 15 | 16 | const navItems = [ 17 | { name: 'Features', href: '#features' }, 18 | { name: 'How It Works', href: '#how-it-works' }, 19 | { name: 'Showcase', href: '#showcase' }, 20 | { name: 'Community', href: '#community' } 21 | ]; 22 | 23 | // Track scroll position for background opacity changes 24 | useEffect(() => { 25 | const handleScroll = () => { 26 | setScrollPosition(window.scrollY); 27 | }; 28 | window.addEventListener('scroll', handleScroll); 29 | return () => window.removeEventListener('scroll', handleScroll); 30 | }, []); 31 | 32 | // Smooth scroll handler 33 | const handleScroll = (e: React.MouseEvent, href: string) => { 34 | e.preventDefault(); 35 | const targetId = href.substring(1); 36 | const targetElement = document.getElementById(targetId); 37 | if (targetElement) { 38 | window.scrollTo({ 39 | top: targetElement.offsetTop - 80, // Offset for navbar height 40 | behavior: 'smooth' 41 | }); 42 | setMobileMenuOpen(false); 43 | } 44 | }; 45 | 46 | // Calculate background opacity based on scroll position 47 | const backgroundOpacity = transparent 48 | ? Math.min(scrollPosition / 300, 0.95) // Gradually increase opacity as user scrolls 49 | : 0.95; // Default opacity 50 | 51 | // Calculate blur based on scroll position 52 | const blurAmount = transparent 53 | ? Math.min(scrollPosition / 100, 10) // Gradually increase blur as user scrolls 54 | : 10; // Default blur 55 | 56 | return ( 57 | 63 |
50 ? '1px solid rgba(75, 75, 75, 0.2)' : 'none', 69 | transition: 'all 0.3s ease-in-out' 70 | }} 71 | > 72 |
73 | {/* Logo */} 74 | 75 | 79 | 80 | 81 | 86 | Fastlytics 87 | 88 | 89 | 90 | {/* Desktop Navigation */} 91 |
92 | 109 | 110 | 111 | 115 | 116 | 117 |
118 | 119 | {/* Mobile Menu Button */} 120 |
121 | 122 | 123 | 127 | 128 | 129 |
130 |
131 | 132 | Fastlytics 133 |
134 | 135 | 147 | 148 |
149 | setMobileMenuOpen(false)}> 150 | 154 | 155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | ); 164 | }; 165 | 166 | export default FloatingNavbar; -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { motion } from 'framer-motion'; 4 | import { 5 | Github, Twitter, Mail, Gauge, Heart, 6 | ExternalLink 7 | } from 'lucide-react'; 8 | 9 | const Footer = () => { 10 | const currentYear = new Date().getFullYear(); 11 | 12 | const fadeInUp = { 13 | initial: { opacity: 0, y: 20 }, 14 | animate: { opacity: 1, y: 0 }, 15 | transition: { duration: 0.5 } 16 | }; 17 | 18 | const featuresLinks = [ 19 | { name: 'Dashboard', href: '/dashboard' }, 20 | { name: 'Races', href: '/races' }, 21 | { name: 'Standings', href: '/standings/drivers' }, 22 | ]; 23 | 24 | const quickLinks = [ 25 | { name: 'Support us', href: 'https://ko-fi.com/fastlytics', external: true }, 26 | { name: 'Discord', href: 'https://discord.gg/bSEGSMwFDn', external: true }, 27 | { name: 'FAQ', href: '/faq' }, 28 | { name: 'Privacy', href: '/privacy-policy' }, 29 | ]; 30 | 31 | const socialLinks = [ 32 | { name: 'Twitter', href: 'https://x.com/fastlytics', icon: }, 33 | { name: 'GitHub', href: 'https://github.com/subhashhhhhh', icon: }, 34 | { name: 'Mail', href: 'mailto:contact@fastlytics.app', icon: }, 35 | ]; 36 | 37 | return ( 38 |
39 | {/* Decorative elements */} 40 |
41 |
42 |
43 | 44 |
45 |
46 | {/* Logo & Description */} 47 |
48 | 52 |
53 | 54 | 59 |
60 | Fastlytics 61 | 62 | 63 |

64 | Advanced F1 analytics and insights for racing fans. 65 |

66 | 67 |
68 | {socialLinks.map((link) => ( 69 | 78 | {link.icon} 79 | 80 | ))} 81 |
82 |
83 | 84 | {/* Navigation Links */} 85 |
86 |

87 | Navigate 88 |

89 |
    90 | {featuresLinks.map((link) => ( 91 |
  • 92 | 96 | {link.name} 97 | 98 |
  • 99 | ))} 100 |
101 |
102 | 103 | {/* Quick Links */} 104 |
105 |

106 | Quick Links 107 |

108 |
    109 | {quickLinks.map((link) => ( 110 |
  • 111 | {link.external ? ( 112 | 118 | {link.name} 119 | 120 | 121 | ) : ( 122 | 126 | {link.name} 127 | 128 | )} 129 |
  • 130 | ))} 131 |
132 |
133 |
134 | 135 | {/* Bottom Bar */} 136 |
137 |
138 | © {currentYear} Fastlytics. All rights reserved. 139 | Not affiliated with Formula 1 140 |
141 |
142 | Made with 143 | 144 | for F1 fans by Subhash 145 |
146 |
147 |
148 |
149 | ); 150 | }; 151 | 152 | export default Footer; 153 | -------------------------------------------------------------------------------- /src/components/KeyMomentsHighlight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; 3 | import { LapPositionDataPoint } from '@/lib/api'; // Use the correct type 4 | import { ArrowDown, ArrowUp, Zap } from 'lucide-react'; 5 | 6 | interface KeyMomentsHighlightProps { 7 | lapData?: LapPositionDataPoint[]; // Use the correct type 8 | } 9 | 10 | const KeyMomentsHighlight: React.FC = ({ lapData }) => { 11 | if (!lapData || lapData.length === 0) { 12 | return null; 13 | } 14 | 15 | let biggestGain = { driver: '', change: 0, lap: 0, from: 0, to: 0 }; 16 | let biggestLoss = { driver: '', change: 0, lap: 0, from: 0, to: 0 }; 17 | 18 | const positionsByLap: { [lap: number]: { [driver: string]: number } } = {}; 19 | 20 | // Organize data by lap and driver 21 | lapData.forEach(lapEntry => { 22 | if (!positionsByLap[lapEntry.lap_number]) { 23 | positionsByLap[lapEntry.lap_number] = {}; 24 | } 25 | positionsByLap[lapEntry.lap_number][lapEntry.driver_code] = lapEntry.position; 26 | }); 27 | 28 | const laps = Object.keys(positionsByLap).map(Number).sort((a, b) => a - b); 29 | 30 | // Iterate through laps to find changes 31 | for (let i = 1; i < laps.length; i++) { 32 | const currentLapNumber = laps[i]; 33 | const previousLapNumber = laps[i - 1]; 34 | const currentLapPositions = positionsByLap[currentLapNumber]; 35 | const previousLapPositions = positionsByLap[previousLapNumber]; 36 | 37 | for (const driver in currentLapPositions) { 38 | if (previousLapPositions && driver in previousLapPositions) { 39 | const currentPos = currentLapPositions[driver]; 40 | const previousPos = previousLapPositions[driver]; 41 | const change = previousPos - currentPos; // Gain is positive, Loss is negative 42 | 43 | if (change > biggestGain.change) { 44 | biggestGain = { driver, change, lap: currentLapNumber, from: previousPos, to: currentPos }; 45 | } 46 | if (change < biggestLoss.change) { 47 | biggestLoss = { driver, change, lap: currentLapNumber, from: previousPos, to: currentPos }; 48 | } 49 | } 50 | } 51 | } 52 | 53 | // Don't render if no significant changes found 54 | if (biggestGain.change <= 1 && biggestLoss.change >= -1) { // Only show if gain > 1 or loss < -1 55 | return null; 56 | } 57 | 58 | return ( 59 | 60 | 61 | Key Moments 62 | Largest single-lap position changes. 63 | 64 | 65 | {biggestGain.change > 1 ? ( 66 |
67 |

68 | Biggest Gain 69 |

70 |

{biggestGain.driver} gained {biggestGain.change} places

71 |

Moved from P{biggestGain.from} to P{biggestGain.to} on Lap {biggestGain.lap}

72 |
73 | ) : ( 74 |
75 |

76 | No Significant Gains 77 |

78 |

No driver gained more than 1 position in a single lap.

79 |
80 | )} 81 | 82 | {biggestLoss.change < -1 ? ( 83 |
84 |

85 | Biggest Loss 86 |

87 |

{biggestLoss.driver} lost {Math.abs(biggestLoss.change)} places

88 |

Moved from P{biggestLoss.from} to P{biggestLoss.to} on Lap {biggestLoss.lap}

89 |
90 | ) : ( 91 |
92 |

93 | No Significant Losses 94 |

95 |

No driver lost more than 1 position in a single lap.

96 |
97 | )} 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default KeyMomentsHighlight; -------------------------------------------------------------------------------- /src/components/MobileWarningBanner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { AlertCircle, X } from 'lucide-react'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface MobileWarningBannerProps { 6 | id: string; // Unique identifier for this warning 7 | expiresInDays?: number; // How many days before showing the warning again 8 | className?: string; 9 | } 10 | 11 | const MobileWarningBanner: React.FC = ({ 12 | id, 13 | expiresInDays = 1, 14 | className 15 | }) => { 16 | const [isVisible, setIsVisible] = useState(false); 17 | const [isMobile, setIsMobile] = useState(false); 18 | 19 | // Check if the device is mobile based on screen size 20 | useEffect(() => { 21 | const checkIfMobile = () => { 22 | setIsMobile(window.innerWidth < 768); 23 | }; 24 | 25 | // Initial check 26 | checkIfMobile(); 27 | 28 | // Add event listener for window resize 29 | window.addEventListener('resize', checkIfMobile); 30 | 31 | // Cleanup 32 | return () => window.removeEventListener('resize', checkIfMobile); 33 | }, []); 34 | 35 | useEffect(() => { 36 | if (!isMobile) { 37 | setIsVisible(false); 38 | return; 39 | } 40 | 41 | // Check if this warning has been dismissed 42 | const dismissedWarnings = JSON.parse(localStorage.getItem('dismissedWarnings') || '{}'); 43 | 44 | // If there's an expiry date stored, check if it's passed 45 | if (dismissedWarnings[id]) { 46 | const expiryDate = new Date(dismissedWarnings[id]); 47 | if (new Date() > expiryDate) { 48 | // Expired, show again 49 | delete dismissedWarnings[id]; 50 | localStorage.setItem('dismissedWarnings', JSON.stringify(dismissedWarnings)); 51 | setIsVisible(true); 52 | } else { 53 | // Not expired, keep hidden 54 | setIsVisible(false); 55 | } 56 | } else { 57 | // Never dismissed, show it 58 | setIsVisible(true); 59 | } 60 | }, [id, expiresInDays, isMobile]); 61 | 62 | const dismissWarning = () => { 63 | const dismissedWarnings = JSON.parse(localStorage.getItem('dismissedWarnings') || '{}'); 64 | 65 | // Set expiry date 66 | const expiryDate = new Date(); 67 | expiryDate.setDate(expiryDate.getDate() + expiresInDays); 68 | 69 | dismissedWarnings[id] = expiryDate.toISOString(); 70 | localStorage.setItem('dismissedWarnings', JSON.stringify(dismissedWarnings)); 71 | 72 | setIsVisible(false); 73 | }; 74 | 75 | if (!isVisible || !isMobile) return null; 76 | 77 | return ( 78 |
82 |
83 | 84 |
85 |

Best viewed on desktop

86 |

87 | For the best experience with charts and data visualizations, we recommend using a larger screen. 88 |

89 |
90 | 97 |
98 |
99 | ); 100 | }; 101 | 102 | export default MobileWarningBanner; -------------------------------------------------------------------------------- /src/components/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | const ScrollToTop = () => { 5 | const { pathname } = useLocation(); 6 | 7 | useEffect(() => { 8 | window.scrollTo(0, 0); 9 | }, [pathname]); 10 | 11 | return null; // This component doesn't render anything 12 | }; 13 | 14 | export default ScrollToTop; -------------------------------------------------------------------------------- /src/components/TeamPaceTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TableCaption } from "@/components/ui/table"; 3 | import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; 4 | import { TeamPaceData } from '@/lib/api'; 5 | import { cn, formatTime } from '@/lib/utils'; // Assuming formatTime exists 6 | 7 | // Helper function to get team color class (Should be centralized) 8 | const getTeamColorClass = (teamName: string | undefined): string => { 9 | if (!teamName) return 'gray'; 10 | const simpleName = teamName.toLowerCase().replace(/[^a-z0-9]/g, ''); 11 | if (simpleName.includes('mclaren')) return 'mclaren'; 12 | if (simpleName.includes('mercedes')) return 'mercedes'; 13 | if (simpleName.includes('redbull')) return 'redbull'; 14 | if (simpleName.includes('ferrari')) return 'ferrari'; 15 | if (simpleName.includes('alpine')) return 'alpine'; 16 | if (simpleName.includes('astonmartin')) return 'astonmartin'; 17 | if (simpleName.includes('williams')) return 'williams'; 18 | if (simpleName.includes('haas')) return 'haas'; 19 | if (simpleName.includes('sauber') || simpleName.includes('kick')) return 'alfaromeo'; 20 | if (simpleName.includes('racingbulls') || simpleName.includes('rb') || simpleName.includes('visa')) return 'alphatauri'; 21 | return 'gray'; 22 | } 23 | 24 | interface TeamPaceTableProps { 25 | teamPaceData?: TeamPaceData[]; 26 | } 27 | 28 | const TeamPaceTable: React.FC = ({ teamPaceData }) => { 29 | if (!teamPaceData || teamPaceData.length === 0) { 30 | return ( 31 | 32 | 33 | Team Race Pace Ranking 34 | Based on median time of laps within 107% of the fastest lap. 35 | 36 | 37 |

No team pace data available for this session.

38 |
39 |
40 | ); 41 | } 42 | 43 | return ( 44 | 45 | 46 | Team Race Pace Ranking 47 | Based on median time of laps within 107% of the fastest lap. 48 | 49 | 50 |
51 | 52 | 53 | 54 | Rank 55 | Team 56 | Median Pace 57 | Average Pace 58 | Std Dev (s) 59 | 60 | 61 | 62 | {teamPaceData.map((team) => { 63 | const teamColor = getTeamColorClass(team.teamName); 64 | return ( 65 | 66 | {team.rank} 67 | 68 | 69 | 70 | {team.teamName} 71 | 72 | 73 | {formatTime(team.medianTime)} 74 | {formatTime(team.averageTime)} 75 | {team.stdDev.toFixed(3)} 76 | 77 | ); 78 | })} 79 | 80 | Std Dev indicates lap time consistency (lower is more consistent). 81 |
82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | export default TeamPaceTable; -------------------------------------------------------------------------------- /src/components/TrackProgress.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | interface TrackProgressProps { 5 | progress: number; 6 | trackColor?: string; 7 | progressColor?: string; 8 | } 9 | 10 | const TrackProgress = ({ 11 | progress, 12 | trackColor = "rgba(255,255,255,0.1)", 13 | progressColor = "#ff2800" 14 | }: TrackProgressProps) => { 15 | const strokeWidth = 6; 16 | const radius = 35; 17 | const circumference = 2 * Math.PI * radius; 18 | const strokeDashoffset = circumference - (progress / 100) * circumference; 19 | 20 | return ( 21 |
22 | 23 | {/* Track */} 24 | 32 | 33 | {/* Progress */} 34 | 47 | 48 | {/* Start/Finish Line */} 49 | 57 | 58 | 59 |
60 | {progress}% 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default TrackProgress; 67 | -------------------------------------------------------------------------------- /src/components/ui/LoadingSpinnerF1.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from '@/lib/utils'; 3 | 4 | interface LoadingSpinnerF1Props { 5 | className?: string; 6 | size?: number; // Size in pixels 7 | } 8 | 9 | const LoadingSpinnerF1: React.FC = ({ 10 | className, 11 | size = 48, // Default size 12 | }) => { 13 | // Simple SVG spinning wheel design 14 | // Colors can be adjusted to match theme (e.g., text-red-500) 15 | return ( 16 | 28 | {/* Outer circle */} 29 | 30 | {/* Inner structure - simplified wheel spokes */} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {/* Optional: Add a moving segment for more dynamism */} 40 | {/* */} 41 | 42 | ); 43 | }; 44 | 45 | export default LoadingSpinnerF1; 46 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDown } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Accordion = AccordionPrimitive.Root 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )) 19 | AccordionItem.displayName = "AccordionItem" 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180", 30 | className 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 36 | 37 | 38 | )) 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
{children}
51 |
52 | )) 53 | 54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 55 | 56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 57 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { buttonVariants } from "@/components/ui/button" 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, ...props }, ref) => ( 17 | 25 | )) 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 33 | 34 | 42 | 43 | )) 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 45 | 46 | const AlertDialogHeader = ({ 47 | className, 48 | ...props 49 | }: React.HTMLAttributes) => ( 50 |
57 | ) 58 | AlertDialogHeader.displayName = "AlertDialogHeader" 59 | 60 | const AlertDialogFooter = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | AlertDialogFooter.displayName = "AlertDialogFooter" 73 | 74 | const AlertDialogTitle = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, ...props }, ref) => ( 78 | 83 | )) 84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 85 | 86 | const AlertDialogDescription = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )) 96 | AlertDialogDescription.displayName = 97 | AlertDialogPrimitive.Description.displayName 98 | 99 | const AlertDialogAction = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 110 | 111 | const AlertDialogCancel = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 124 | )) 125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 126 | 127 | export { 128 | AlertDialog, 129 | AlertDialogPortal, 130 | AlertDialogOverlay, 131 | AlertDialogTrigger, 132 | AlertDialogContent, 133 | AlertDialogHeader, 134 | AlertDialogFooter, 135 | AlertDialogTitle, 136 | AlertDialogDescription, 137 | AlertDialogAction, 138 | AlertDialogCancel, 139 | } 140 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "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", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } 6 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "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", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>